Schnelles UICollectionView-Layout in einem Hintergrundthread

Sie können einen Originalbeitrag sehen hier
Wir alle wollen schnelles UICollectionView mit 60 fps. Und am Anfang ist es in einfachen Fällen einfach. Dann will der Kunde Schritt für Schritt unsere Sammelzellen verbessern: immer mehr Informationen in unseren Zellen anzeigen, dynamisieren, mehr Bilder oder Texte hinzufügen etc. Und es gibt viele Tipps zur Performance-Verbesserung, zB: nicht Verwenden Sie die automatische Dimension, verwenden Sie keine Einschränkungen, erstellen Sie Datenmodellstrukturen mit vorverarbeiteten Daten, anstatt JSON zu analysieren configureCell Methode und so weiter. Um unsere UI-Erfahrung zu verbessern, können wir wirklich leistungsstarke Bibliotheken wie verwenden Textur. Aber in Texture funktionieren viele Dinge wie Magie, die wir nicht kontrollieren können. Und wenn Sie mehr Kontrolle über diesen Prozess erlangen möchten, möchte ich Ihnen einen weiteren Trick zeigen, um Layoutberechnungen in einen Hintergrundthread zu verschieben.
Wenn wir normalerweise die Größe einer Zelle im Hintergrund berechnen und das Ergebnis zwischenspeichern (für eine schnelle heightForRowAtIndexPath Methode) machen wir so etwas (zumindest habe ich das früher gemacht):

func heightForModel(dataModel: DataModel) -> CGFloat {
    let stringHeight = calculateHeightForText(dataModel.text)
    let commentsHeight = dataModel.comments.count > 0 ? Constant.commentsPanelHeight : 0
    return Constats.topPadding + Constants.titleHeight + stringHeight + commentsHeight
}

Aber dieser Ansatz hat zwei Hauptnachteile:

  1. Wir haben zwei Teile des Layoutcodes an zwei Orten gespeichert. Der erste Teil – in der Höhenberechnungsfunktion und der zweite – in derdidLayouSubviews Methode in einer Zelle.

  2. Wir cachen Ergebnisse dieser Funktion, aber wir sollten auch das Layout bei jedem Aufruf von neu berechnen cellForRowAtIndexPath Methode. Und dadurch belasten wir unsere CPU mehr als wir könnten.

Die Lösung besteht darin, alle Layoutberechnungen in den Hintergrund zu verlagern und alle diese Ergebnisse zwischenzuspeichern. Wir werden dies tun, indem wir einige viewModels einführen (achten Sie nicht viel auf den Layout-Code unten):

class LabelModel {
    var text: NSAttributedString
    var frame: CGRect
  
    func layout(with maxSize: CGSize) {
        let textContainer = NSTextContainer(size: maxSize)
        textContainer.maximumNumberOfLines = 0
        textContainer.lineFragmentPadding = 0
        let textStorage = NSTextStorage(attributedString: self.text)
        
        let layoutManager = NSLayoutManager()
        layoutManager.addTextContainer(textContainer)
        
        textStorage.addLayoutManager(layoutManager)
        
        self.frame = CGRect(origin: CGPoint(x: 0, y: 0),
                            size: layoutManager.usedRect(for: textContainer).size)
    }
}

Und wir wenden dieses Modell dann auf ein UILabel an:

extension UILabel {
    func applyModel(_ model: LabelModel) {
        self.attributedText = model.text
        self.frame = model.frame
    }
}

Die Hauptidee ist die folgende: Wir berechnen einen Rahmen auf dem Hintergrund-Thread, speichern ihn und wenden ihn dann auf das echte Etikett auf dem Haupt-Thread an.

Stellen wir uns vor, wir haben eine Zelle mit zwei Beschriftungen: eine Textbeschriftung und eine Likes-Zählbeschriftung. Für diese Zelle könnte viewModel ähnlich sein wie:

class TestCellModel {
    var textModel: LabelModel
    var likesModel: LabelModel
    var frame: CGRect
}

Und der Layoutcode wäre:

func layout(with maxSize: CGSize) {
        
        let likesMaxSize = CGSize(width: maxSize.width,
                               height: CGFloat.greatestFiniteMagnitude)
        likesModel.layout(with: likesMaxSize)
        
        
        let textWidthRestriction = maxSize.width - likesModel.frame.width - Constants.interItemSpacing
        
        let textMaxSize = CGSize(width: textWidthRestriction,
                              height: CGFloat.greatestFiniteMagnitude)
        textModel.layout(with: textMaxSize)
        
        textModel.frame.origin = CGPoint(x: 0, y: Constants.verticalPadding)
        likesModel.frame.origin = CGPoint(x: textWidthRestriction + Constants.interItemSpacing,
                                          y: Constants.verticalPadding)
        
        self.frame = CGRect(x: 0,
                            y: 0,
                            width: maxSize.width,
                            height: textModel.frame.height + Constants.verticalPadding * 2)
}

Tatsächlich spielt es keine Rolle, was Layout-Code tut. Es ist ein allgemeines Konzept.

Nachdem wir nun alle Vorbereitungen getroffen haben, müssen wir nur noch unsere TestCell-Klasse erstellen:

class TestCell: UICollectionViewCell {

    private let textLabel: UILabel = UILabel()
    private let likesLabel: UILabel = UILabel()
    private var model: TestCellModel = TestCellModel.zero() // for avoiding optionals
    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(textLabel)
        addSubview(likesLabel)
    }

    func applyModel(_ model: TestCellModel) {
        self.model = model
        setNeedsLayout() // for avoiding multiple setters in fast scroll
    }

    override func layoutSubviews() {
        textLabel.applyModel(self.model.textModel)
        likesLabel.applyModel(self.model.likesModel)
    }
}

Am Ende haben wir also ein Modell für das Kalkulationslayout im Hintergrund. Wir können es zwischenspeichern und was noch nützlicher ist – wir können dieses Modell auf unsere Zelle anwenden und es ist eine wirklich billige Operation. Außerdem haben wir den Layoutcode nur an einer Stelle gespeichert.

Dies ist nur ein Konzept, aber es funktioniert gut, wenn wir unsere Sammlungsleistung verbessern möchten und viele umfangreiche Layout-Operationen haben.

Ich hoffe, dieser Ansatz hilft Ihnen bei Ihren Projekten.

Beispielprojekt ist auf der verfügbar GitHub

Danke fürs Lesen.

Similar Posts

Leave a Reply

Your email address will not be published.