草庐IT

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

coder 2024-01-28 原文

我的问题是 UITableView 在滚动时滞后很多。

This is what I am trying to achieve

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

所以这意味着内容显示如下:

  • 部分标题(包含日期和日期)
  • Custom UITableViewCell = header(包含一些header信息)
  • 自定义 UITableViewCell(包含要显示的数据)

这是我的代码:

cellForRowAt:

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

        let row = indexPath.row

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

            let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! ObjednavkyTableViewCell
            cell.updateData(objednavka: chod.objednavky[row-semiResult], canSetAmount: self.typDialogu == 3)
            return cell
        }
    }

checkIfIsHeader:

func checkIfIsHeader(section: Int, row: Int) -> (Bool, Int, Int){
        if let cachedResult = checkIfHeaderCache[section]?[row] {
            return (cachedResult[0] == 1, cachedResult[1], cachedResult[2])
        }

        var isHeader = false

        var semiResult = 0
        var headerNumber = -1

        for (index, chod) in objednavkaDny[section].chody.enumerated() {
            let sum = chod.objednavky.count
            if row == semiResult {
                isHeader = true
                break
            }else if row < semiResult {
                semiResult -= objednavkaDny[section].chody[index-1].objednavky.count
                break
            }else {
                headerNumber += 1
                semiResult += 1
                if index != objednavkaDny[section].chody.count - 1 {
                    semiResult += sum
                }
            }
        }
        checkIfHeaderCache[section] = [Int:[Int]]()
        checkIfHeaderCache[section]![row] = [isHeader ? 1 : 0, headerNumber, semiResult]
        return (isHeader, headerNumber, semiResult)
    }

以及显示数据的主单元格:

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: ".SFUIText", 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: "arrow-right")?.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("init(coder:) has not been implemented")
    }


    @objc func detailsButtonPressed() {

    }


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


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

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

    func checkIfSelected(objects: [ObjednavkaObject]) {
        var didChangeSelection = false
        for object in objects {          // Checks wether cell should be selected or not
            if object.pocet > 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: [ObjednavkaObject]) {
        let subviews = pricesContainerView.subviews
        var subviewsToDelete = subviews.count

        for (index, object) in objects.enumerated() {
            subviewsToDelete -= 1
            if subviews.count - 1 >= index {
                let priceView = subviews[index] as! ObjednavkyPriceView
                priceView.titleLabel.text = object.popisProduktu  // + " " + NSNumber(value: object.cena).getFormattedString(currencySymbol: "Kč") // TODO: currencySymbol
                priceView.canSetAmount = canSetAmount
                priceView.count = object.pocet
                priceView.canOrder = (object.nelzeObj == nil || object.nelzeObj == "")
            }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  // + " " + NSNumber(value: object.cena).getFormattedString(currencySymbol: "Kč") // TODO: currencySymbol
                priceView.numberTextField.delegate = self
                priceView.canSetAmount = canSetAmount
                priceView.canOrder = (object.nelzeObj == nil || object.nelzeObj == "")
                priceView.count = object.pocet
                pricesContainerHeight += ((index == 0) ? 30 : 35)
            }
        }

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

        if pricesContainerHeight < 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
    }
}

总结一下它是如何完成的:

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

我还注意到它在某些滚动级别滞后,有时它工作得很好,有时它滞后很多。

尽管我进行了所有优化,但 tableView 在滚动时仍然滞后。

Here is a screenshot from Instruments

非常感谢任何有关如何提高滚动性能的提示!

(我可能忘记包含一些代码/信息,所以请随时在评论中问我。)

最佳答案

我无法告诉您延迟发生的确切位置,但当我们谈论滚动期间的延迟时,它与您的 cellForRowAt 委托(delegate)方法有关。发生的事情是在这个方法中发生了太多事情,并且它被调用用于显示和将要显示的每个单元格。我看到您正在尝试通过 checkIfHeaderCache 缓存结果,但仍然在最开始有一个 for 循环来确定标题单元格。

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

if (isHeader) {
  render header cell
} else {
  render other cell
}

底线: cellForRow 方法不是为处理繁重的计算而设计的,如果这样做会减慢滚动速度。此方法仅用于将值分配给缓存的 TableView 单元格,这是它唯一擅长的事情。

关于ios - 带有自定义单元格的 UITableView 在滚动时滞后,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50116654/

有关ios - 带有自定义单元格的 UITableView 在滚动时滞后的更多相关文章

  1. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  2. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  3. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  4. ruby - 主要 :Object when running build from sublime 的未定义方法 `require_relative' - 2

    我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby​​1.9+ 关于ruby-主要:Objectwhenrun

  5. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  6. ruby - 定义方法参数的条件 - 2

    我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano

  7. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  8. ruby - 如何在 Grape 中定义哈希数组? - 2

    我使用Ember作为我的前端和GrapeAPI来为我的API提供服务。前端发送类似:{"service"=>{"name"=>"Name","duration"=>"30","user"=>nil,"organization"=>"org","category"=>nil,"description"=>"description","disabled"=>true,"color"=>nil,"availabilities"=>[{"day"=>"Saturday","enabled"=>false,"timeSlots"=>[{"startAt"=>"09:00AM","endAt"=>

  9. ruby - 获取模块中定义的所有常量的值 - 2

    我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c

  10. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

随机推荐