菜鸟教程小白 发表于 2022-12-11 20:06:11

ios - 滚动时带有自定义单元格的 UITableView 滞后


                                            <p><p>我的问题是 <code>UITableView</code> 在滚动时滞后很多。</p>

<p> <a href="/image/IL12P.jpg" rel="noreferrer noopener nofollow">This is what I am trying to achieve</a> </p>

<p>从顶部开始,我有一个简单的部分标题,只有一个复选框和一个 <code>UILabel</code>。在此标题下,您可以看到一个自定义单元格,其中只有一个 <code>UILabel</code> 与中心对齐。这个自定义单元格就像下面显示的数据的另一个标题(基本上是一个 3D 数组)。在这些“标题”下是包含一个多行 <code>UILabel</code> 的自定义单元格,在此标签下是一个容器,用于容纳可变数量的行,其中包含一个复选框和一个 <code>UILabel</code>。单元格右侧还有一个按钮(蓝/白箭头)。</p>

<p>所以这意味着内容是这样显示的:</p>

<ul>
<li>部分标题(包含日期和日期)</li>
<li>自定义 UITableViewCell = header(包含一些头部信息)</li>
<li>自定义 UITableViewCell(包含要显示的数据)</li>
</ul>

<p>这是我的代码:</p>

<p><strong>cellForRowAt:</strong></p>

<pre><code>override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -&gt; UITableViewCell {
      let (isHeader, headerNumber, semiResult) = checkIfIsHeader(section: indexPath.section, row: indexPath.row)

      let row = indexPath.row

      if isHeader {
            let chod = objednavkaDny.chody
            let cell = tableView.dequeueReusableCell(withIdentifier: cellHeaderReuseIdentifier, for: indexPath) as! ObjednavkyHeaderTableViewCell
            cell.titleLabel.text = chod.popisPoradiJidla
            cell.selectionStyle = .none
            return cell
      }else{
            let chod = objednavkaDny.chody

            let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! ObjednavkyTableViewCell
            cell.updateData(objednavka: chod.objednavky, canSetAmount: self.typDialogu == 3)
            return cell
      }
    }
</code></pre>

<p><strong>checkIfIsHeader:</strong></p>

<pre><code>func checkIfIsHeader(section: Int, row: Int) -&gt; (Bool, Int, Int){
      if let cachedResult = checkIfHeaderCache? {
            return (cachedResult == 1, cachedResult, cachedResult)
      }

      var isHeader = false

      var semiResult = 0
      var headerNumber = -1

      for (index, chod) in objednavkaDny.chody.enumerated() {
            let sum = chod.objednavky.count
            if row == semiResult {
                isHeader = true
                break
            }else if row &lt; semiResult {
                semiResult -= objednavkaDny.chody.objednavky.count
                break
            }else {
                headerNumber += 1
                semiResult += 1
                if index != objednavkaDny.chody.count - 1 {
                  semiResult += sum
                }
            }
      }
      checkIfHeaderCache = ]()
      checkIfHeaderCache! =
      return (isHeader, headerNumber, semiResult)
    }
</code></pre>

<p><strong>以及显示数据的主单元格:</strong></p>

<pre><code>class ObjednavkyTableViewCell: UITableViewCell {

    lazy var numberTextField: ObjednavkyTextField = {
      let textField = ObjednavkyTextField()
      textField.translatesAutoresizingMaskIntoConstraints = false
      return textField
    }()

    let mealLabel: UILabel = {
      let label = UILabel()
      label.translatesAutoresizingMaskIntoConstraints = false
      label.textColor = .black
      label.textAlignment = .left
      label.font = UIFont(name: &#34;.SFUIText&#34;, size: 15)
      label.numberOfLines = 0
      label.backgroundColor = .white
      label.isOpaque = true
      return label
    }()


    lazy var detailsButton: UIButton = {
      let button = UIButton(type: .custom)
      button.translatesAutoresizingMaskIntoConstraints = false
      button.setImage(UIImage(named: &#34;arrow-right&#34;)?.withRenderingMode(.alwaysTemplate), for: .normal)
      button.imageView?.tintColor = UIColor.custom.blue.classicBlue
      button.imageView?.contentMode = .scaleAspectFit
      button.contentHorizontalAlignment = .right
      button.imageEdgeInsets = UIEdgeInsetsMake(10, 0, 10, 0)
      button.addTarget(self, action: #selector(detailsButtonPressed), for: .touchUpInside)
      button.backgroundColor = .white
      button.isOpaque = true
      return button
    }()

    let pricesContainerView: UIView = {
      let view = UIView()
      view.translatesAutoresizingMaskIntoConstraints = false
      view.backgroundColor = .white
      view.isOpaque = true
      return view
    }()


    var canSetAmount = false {
      didSet {
            canSetAmount ? showNumberTextField() : hideNumberTextField()
      }
    }


    var shouldShowPrices = false {
      didSet {
            shouldShowPrices ? showPricesContainerView() : hidePricesContainerView()
      }
    }

    var pricesContainerHeight: CGFloat = 0

    private let priceViewHeight: CGFloat = 30

    var mealLabelLeadingConstraint: NSLayoutConstraint?
    var mealLabelBottomConstraint: NSLayoutConstraint?
    var pricesContainerViewHeightConstraint: NSLayoutConstraint?
    var pricesContainerViewBottomConstraint: NSLayoutConstraint?

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
      super.init(style: style, reuseIdentifier: reuseIdentifier)
      self.selectionStyle = .none
      setupView()
    }

    required init?(coder aDecoder: NSCoder) {
      fatalError(&#34;init(coder:) has not been implemented&#34;)
    }


    @objc func detailsButtonPressed() {

    }


    func updateData(objednavka: Objednavka, canSetAmount: Bool) {
      self.canSetAmount = canSetAmount
      if let popisJidla = objednavka.popisJidla, popisJidla != &#34;&#34;, popisJidla != &#34; &#34; {
            self.mealLabel.text = popisJidla
      }else{
            self.mealLabel.text = objednavka.nazevJidelnicku
      }


      if objednavka.objects.count &gt; 1 {
            shouldShowPrices = true
            setPricesStackView(with: objednavka.objects)
            checkIfSelected(objects: objednavka.objects)
      }else{
            shouldShowPrices = false
            self.numberTextField.text = String(objednavka.objects.pocet)
            //setSelected(objednavka.objects.pocet &gt; 0, animated: false)
            objednavka.objects.pocet &gt; 0 ? setSelectedStyle() : setDeselectedStyle()
      }
    }

    //---------------

    func checkIfSelected(objects: ) {
      var didChangeSelection = false
      for object in objects {          // Checks wether cell should be selected or not
            if object.pocet &gt; 0 {
                setSelected(true, animated: false)
                setSelectedStyle()
                didChangeSelection = true
                break
            }
      }
      if !didChangeSelection {
            setSelected(false, animated: false)
            setDeselectedStyle()
      }
    }


    //--------------

    func showNumberTextField() {
      numberTextField.isHidden = false

      mealLabelLeadingConstraint?.isActive = false
      mealLabelLeadingConstraint = mealLabel.leadingAnchor.constraint(equalTo: numberTextField.trailingAnchor, constant: 10)
      mealLabelLeadingConstraint?.isActive = true
    }

    func hideNumberTextField() {
      numberTextField.isHidden = true

      mealLabelLeadingConstraint?.isActive = false
      mealLabelLeadingConstraint = mealLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor, constant: 0)
      mealLabelLeadingConstraint?.isActive = true
    }

    func showPricesContainerView() {
      hideNumberTextField()

      pricesContainerView.isHidden = false

      mealLabelBottomConstraint?.isActive = false
      pricesContainerViewBottomConstraint?.isActive = true
    }

    func hidePricesContainerView() {
      pricesContainerView.isHidden = true

      pricesContainerViewBottomConstraint?.isActive = false
      mealLabelBottomConstraint?.isActive = true
    }

    //--------------

    func setSelectedStyle() {
      self.backgroundColor = UIColor.custom.blue.classicBlue
      mealLabel.textColor = .white
      mealLabel.backgroundColor = UIColor.custom.blue.classicBlue

      for subview in pricesContainerView.subviews where subview is ObjednavkyPriceView {
            let priceView = (subview as! ObjednavkyPriceView)
            priceView.titleLabel.textColor = .white
            priceView.checkBox.backgroundColor = UIColor.custom.blue.classicBlue
            priceView.titleLabel.backgroundColor = UIColor.custom.blue.classicBlue
            priceView.backgroundColor = UIColor.custom.blue.classicBlue
      }

      pricesContainerView.backgroundColor = UIColor.custom.blue.classicBlue

      detailsButton.imageView?.tintColor = .white
      detailsButton.backgroundColor = UIColor.custom.blue.classicBlue

    }

    func setDeselectedStyle() {
      self.backgroundColor = .white
      mealLabel.textColor = .black
      mealLabel.backgroundColor = .white

      for subview in pricesContainerView.subviews where subview is ObjednavkyPriceView {
            let priceView = (subview as! ObjednavkyPriceView)
            priceView.titleLabel.textColor = .black
            priceView.checkBox.backgroundColor = .white
            priceView.titleLabel.backgroundColor = .white
            priceView.backgroundColor = .white
      }

      pricesContainerView.backgroundColor = .white

      detailsButton.imageView?.tintColor = UIColor.custom.blue.classicBlue
      detailsButton.backgroundColor = .white
    }

    //-----------------

    func setPricesStackView(with objects: ) {
      let subviews = pricesContainerView.subviews
      var subviewsToDelete = subviews.count

      for (index, object) in objects.enumerated() {
            subviewsToDelete -= 1
            if subviews.count - 1 &gt;= index {
                let priceView = subviews as! ObjednavkyPriceView
                priceView.titleLabel.text = object.popisProduktu// + &#34; &#34; + NSNumber(value: object.cena).getFormattedString(currencySymbol: &#34;Kč&#34;) // TODO: currencySymbol
                priceView.canSetAmount = canSetAmount
                priceView.count = object.pocet
                priceView.canOrder = (object.nelzeObj == nil || object.nelzeObj == &#34;&#34;)
            }else {
                let priceView = ObjednavkyPriceView(frame: CGRect(x: 0, y: CGFloat(index) * priceViewHeight + CGFloat(index * 5), width: pricesContainerView.frame.width, height: priceViewHeight))
                pricesContainerView.addSubview(priceView)
                priceView.titleLabel.text = object.popisProduktu// + &#34; &#34; + NSNumber(value: object.cena).getFormattedString(currencySymbol: &#34;Kč&#34;) // TODO: currencySymbol
                priceView.numberTextField.delegate = self
                priceView.canSetAmount = canSetAmount
                priceView.canOrder = (object.nelzeObj == nil || object.nelzeObj == &#34;&#34;)
                priceView.count = object.pocet
                pricesContainerHeight += ((index == 0) ? 30 : 35)
            }
      }

      if subviewsToDelete &gt; 0 {// Deletes unwanted subviews
            for _ in 0..&lt;subviewsToDelete {
                pricesContainerView.subviews.last?.removeFromSuperview()
                pricesContainerHeight -= pricesContainerHeight + 5
            }
      }

      if pricesContainerHeight &lt; 0 {
            pricesContainerHeight = 0
      }

      pricesContainerViewHeightConstraint?.constant = pricesContainerHeight
    }

    func setupView() {
      self.layer.shouldRasterize = true
      self.layer.rasterizationScale = UIScreen.main.scale

      self.backgroundColor = .white

      contentView.addSubview(numberTextField)
      contentView.addSubview(mealLabel)
      contentView.addSubview(detailsButton)
      contentView.addSubview(pricesContainerView)

      setupConstraints()
    }

    func setupConstraints() {
      numberTextField.anchor(leading: readableContentGuide.leadingAnchor, size: CGSize(width: 30, height: 30))
      numberTextField.centerYAnchor.constraint(equalTo: mealLabel.centerYAnchor).isActive = true

      detailsButton.anchor(trailing: readableContentGuide.trailingAnchor, size: CGSize(width: 30, height: 30))
      detailsButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true

      mealLabel.anchor(top: contentView.topAnchor, trailing: detailsButton.leadingAnchor, padding: .init(top: 10, left: 0, bottom: 0, right: -10))
      mealLabelBottomConstraint = mealLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
      mealLabelBottomConstraint?.priority = UILayoutPriority(rawValue: 999)

      pricesContainerView.anchor(top: mealLabel.bottomAnchor, leading: readableContentGuide.leadingAnchor, trailing: detailsButton.leadingAnchor, padding: .init(top: 10, left: 0, bottom: 0, right: -10))
      pricesContainerViewBottomConstraint = pricesContainerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10)
      pricesContainerViewBottomConstraint?.priority = UILayoutPriority(rawValue: 999)

      pricesContainerViewHeightConstraint = pricesContainerView.heightAnchor.constraint(equalToConstant: 0)
      pricesContainerViewHeightConstraint?.priority = UILayoutPriority(rawValue: 999)
      pricesContainerViewHeightConstraint?.isActive = true
    }
}
</code></pre>

<p>总结一下它是如何完成的:</p>

<ul>
<li><code>tableView.rowHeight</code> 设置为 <code>UITableViewAutomaticDymension</code></li>
<li>在 <code>cellForRowAt</code> 中,我从数组中获取数据并将其提供给
细胞</li>
<li>所有单元格都使用约束在代码中设置</li>
<li>所有 View 都设置了<code>isOpaque = true</code></li>
<li>缓存单元格的高度</li>
<li>单元格设置为栅格化</li>
</ul>

<p>我还注意到它在某些滚动级别时会滞后,有时效果很好,有时会滞后很多。</p>

<p>尽管我做了所有的优化,tableView 在滚动时仍然滞后。</p>

<p> <a href="/image/tKcfc.png" rel="noreferrer noopener nofollow">Here is a screenshot from Instruments</a> </p>

<p>非常感谢任何如何提高滚动性能的提示!</p>

<p>(我可能忘记包含一些代码/信息,请随时在评论中问我。)</p></p>
                                    <br><hr><h1><strong>Best Answer-推荐答案</ strong></h1><br>
                                            <p><p>我无法告诉您延迟究竟发生在哪里,但当我们谈论滚动期间的延迟时,它与您的 <code>cellForRowAt</code> 委托(delegate)方法有关。发生的事情是这个方法中发生了太多事情,并且每个正在显示和将要显示的单元格都会调用它。我看到您正在尝试通过 <code>checkIfHeaderCache</code> 缓存结果,但仍然在开始时有一个 for 循环来确定标题单元格。</p>

<p><strong>建议:</strong>
我不知道你从哪里得到数据(<code>objednavkaDny</code>),但是在你得到数据之后,做一个完整的循环并逐个确定单元格类型,并将结果保存在你的某个位置设计。在此加载期间,您可以在屏幕上显示一些加载消息。然后,在 <code>cellForRow</code> 方法中,你应该只是简单地使用像</p>这样的东西

<pre><code>if (isHeader) {
render header cell
} else {
render other cell
}
</code></pre>

<p><strong>底线:</strong>
<code>cellForRow</code> 方法不是为处理繁重的计算而设计的,如果这样做会减慢滚动速度。此方法仅用于将值分配给缓存的表格 View 单元格,这是它唯一擅长的。</p></p>
                                   
                                                <p style="font-size: 20px;">关于ios - 滚动时带有自定义单元格的 UITableView 滞后,我们在Stack Overflow上找到一个类似的问题:
                                                        <a href="https://stackoverflow.com/questions/50116654/" rel="noreferrer noopener nofollow" style="color: red;">
                                                                https://stackoverflow.com/questions/50116654/
                                                        </a>
                                                </p>
                                       
页: [1]
查看完整版本: ios - 滚动时带有自定义单元格的 UITableView 滞后