【问题标题】:UITableView with autolayout not smooth when scrolling滚动时具有自动布局的 UITableView 不平滑
【发布时间】:2016-07-15 20:32:54
【问题描述】:

我正在使用 XIB 文件来设计 UITableView 中的单元格。我也使用出队机制,例如:let cell = tableView.dequeueReusableCellWithIdentifier("articleCell", forIndexPath: indexPath) as! ArticleTableViewCell。我在 ViewController 的 viewDidLoad 中预先计算了所有行高,因此 func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat 方法立即返回正确的值。所有这些都有效。

在我的 UITableViewCell 中,我使用了许多动态高度标签(行 = 0)。布局是这样的:

我不使用透明背景,我所有的子视图都是不透明的,具有指定的背景颜色。我检查了Color Blended Layers(一切都是绿色)和Color Misaligned Images(一切都不是黄色)。

这是我的 cellForRowAtIndexPath 方法:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let tableViewSection = tableViewSectionAtIndex(indexPath.section)
    let tableViewRow = tableViewSection.rows.objectAtIndex(indexPath.row) as! TableViewRow

    switch tableViewRow.type! {

    case TableViewRowType.Article :
        let article = tableViewRow.article!

        if article.type == TypeArticle.Article {

            let cell = tableView.dequeueReusableCellWithIdentifier("articleCell", forIndexPath: indexPath) as! ArticleTableViewCell
            return cell

        } else {

            let cell = tableView.dequeueReusableCellWithIdentifier("chroniqueCell", forIndexPath: indexPath) as! ChroniqueTableViewCell
            return cell

        }

    default:
        return UITableViewCell()

    }
}

然后,在 willDisplayCell 方法中:

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    let tableViewSection = tableViewSectionAtIndex(indexPath.section)
    let tableViewRow = tableViewSection.rows.objectAtIndex(indexPath.row) as! TableViewRow
    let article = tableViewRow.article!

    if cell.isKindOfClass(ArticleTableViewCell) {
        let cell = cell as! ArticleTableViewCell
        cell.delegate = self
        cell.article = article

        if let imageView = articleImageCache[article.id] {
            cell.articleImage.image = imageView
            cell.shareControl.image = imageView
        } else {
            loadArticleImage(article, articleCell: cell)
        }
    } else {
        let cell = cell as! ChroniqueTableViewCell
        cell.delegate = self
        cell.article = article

        if let chroniqueur = article.getChroniqueur() {
            if let imageView = chroniqueurImageCache[chroniqueur.id] {
                cell.chroniqueurImage.image = imageView
            } else {
                loadChroniqueurImage(article, articleCell: cell)
            }
        }
    }
}

我所有的图片都是在后台线程中下载的,所以滚动时没有图片加载。

当我设置“文章”属性时,我的ArticleTableViewCell中的布局被修改:cell.article = article

var article: Article? {
    didSet {
        updateUI()
    }
}

还有我的 updateUI 函数:

func updateUI() -> Void {

    if let article = article {
        if let surtitre = article.surtitre {
            self.surtitre.text = surtitre.uppercaseString
            self.surtitre.setLineHeight(3)
        } else {
            self.surtitre.hidden = true
        }

        self.titre.text = article.titre
        self.titre.setLineHeight(3)

        if let amorce = article.amorce {
            self.amorce.text = amorce
            self.amorce.setLineHeight(3)
        } else {
            self.amorce.hidden = true
        }

        if let section = article.sectionSource {
            if section.couleurFoncee != "" {
                self.bordureSection.backgroundColor = UIColor(hexString: section.couleurFoncee)
                self.surtitre.textColor = UIColor(hexString: section.couleurFoncee)
            }
        }
    }
}

问题是在设置标签文本时,这会导致滞后。 setLineHeight 方法将标签的文本转换为 NSAttributedString 以指定行高,但即使删除此代码并简单地设置文本标签,显示新单元格时也会出现小延迟。

如果我删除所有标签设置代码,单元格会显示默认标签文本,并且 tableview 滚动非常平滑,高度也正确。每当我设置标签文本时,就会出现滞后。

我正在我的 iPhone 6s 上运行该应用程序。在6s模拟器上,绝对没有卡顿,非常流畅。

有什么想法吗?也许是因为我使用 UIStackView 嵌入我的标签?我这样做是因为在空标签时更容易隐藏标签,因此其他元素会向上移动,以避免在空标签所在的位置出现间距。

我尝试了很多东西,但无法让 tableview 平滑滚动,我们将不胜感激。

【问题讨论】:

  • 感谢您的提示。我问是因为可能有人遇到同样的问题,这对我有很大帮助。无论如何,我都会尝试使用 Instruments 找出答案。
  • 顺便说一句,延迟不是以秒为单位的...但是您可以看到,在下一个表格单元出现之前,当滚动缓慢时,滚动并不流畅。希望仪器能帮助我弄清楚我还没有使用过的工具。我应该使用 Instrument 中的什么工具?
  • 我做了一些优化。我调整了图像的大小,使 UIImage 的大小与 UIImageView 的大小相同。我在 prepareForReuse 方法中的 UIImageView 中加载了一个临时占位符 UIImage,因此如果单元格没有要显示的图像,它将显示此占位符。此图像是从“磁盘”加载的,并造成了一点延迟。我现在使用相同的 UIImage 实例来避免这种情况。现在,唯一的问题是,当从 NIB 创建一个新单元格(没有要出列的单元格)时,会有一个小的延迟,当有足够的单元格要出列时,这个延迟就不存在了。有什么办法解决这个问题?
  • 表格显示时,有2个可见单元格。滚动时,第三个单元格出现,而第一个和第二个单元格仍在屏幕上。
  • 不太明白...在heightForRowAtIndexPath中返回了正确的高度,它为“第0行,第0行”(单元格#1)调用了3次此方法,并为“第1节”调用了4次, row 0" (cell #2) 加载表格时,这些是显示表格时可见的两个单元格。实例化了 2 个笔尖(我在 UITableViewCell 的awakeFromNib 中打印了一个字符串)。我的单元格以正确的高度正确显示。

标签: ios iphone uitableview swift2


【解决方案1】:

我所有的优化使它在 6s 设备上几乎 100% 流畅,但在 5s 上,它真的不流畅。我什至不想在 4s 设备上测试!使用多个多行标签时,我达到了自动布局性能限制。

经过对时间分析器的深入分析,结果是动态标签高度(在我的例子中为 3)与它们之间的约束,并且这些标签具有属性文本(用于设置行高,但这不是瓶颈),似乎滞后是由呈现标签、更新约束等的 UIView::layoutSubviews 引起的......这就是为什么当我不更改标签文本时,一切都很顺利。这里唯一的解决方案是不要在自定义 UITableViewCell 子类的 layoutSubviews 方法中以编程方式使用 autolayout 和 layout de subviews。

对于那些想知道如何做到这一点的人,我实现了在没有自动布局和具有动态高度(多行)的多个标签的情况下实现 100% 平滑滚动。这是我的 UITableView 子类(我使用基类,因为我有 2 种相似的单元格类型):

//
//  ArticleTableViewCell.swift
//

import UIKit

class ArticleTableViewCell: BaseArticleTableViewCell {

    var articleImage = UIImageView()
    var surtitre = UILabel()
    var titre = UILabel()
    var amorce = UILabel()
    var bordureTop = UIView()
    var bordureLeft = UIView()

    var articleImageWidth = CGFloat(0)
    var articleImageHeight = CGFloat(0)

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        self.articleImage.clipsToBounds = true
        self.articleImage.contentMode = UIViewContentMode.ScaleAspectFill

        self.bordureTop.backgroundColor = UIColor(colorLiteralRed: 219/255, green: 219/255, blue: 219/255, alpha: 1.0)

        self.bordureLeft.backgroundColor = UIColor.blackColor()

        self.surtitre.numberOfLines = 0
        self.surtitre.font = UIFont(name: "Graphik-Bold", size: 11)
        self.surtitre.textColor = UIColor.blackColor()
        self.surtitre.backgroundColor = self.contentView.backgroundColor

        self.titre.numberOfLines = 0
        self.titre.font = UIFont(name: "PublicoHeadline-Extrabold", size: 22)
        self.titre.textColor = UIColor(colorLiteralRed: 26/255, green: 26/255, blue: 26/255, alpha: 1.0)
        self.titre.backgroundColor = self.contentView.backgroundColor

        self.amorce.numberOfLines = 0
        self.amorce.font = UIFont(name: "Graphik-Regular", size: 12)
        self.amorce.textColor = UIColor.blackColor()
        self.amorce.backgroundColor = self.contentView.backgroundColor

        self.contentView.addSubview(articleImage)
        self.contentView.addSubview(surtitre)
        self.contentView.addSubview(titre)
        self.contentView.addSubview(amorce)
        self.contentView.addSubview(bordureTop)
        self.contentView.addSubview(bordureLeft)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {

        super.layoutSubviews()

        if let article = article {

            var currentY = CGFloat(0)
            let labelX = CGFloat(18)
            let labelWidth = fullWidth - 48

            // Taille de l'image avec un ratio de 372/243
            articleImageWidth = ceil(fullWidth - 3)
            articleImageHeight = ceil((articleImageWidth * 243) / 372)

            self.bordureTop.frame = CGRect(x: 3, y: 0, width: fullWidth - 3, height: 1)

            // Image
            if article.imagePrincipale == nil {
                self.articleImage.frame = CGRect(x: 0, y: 0, width: 0, height: 0)

                self.bordureTop.hidden = false
            } else {
                self.articleImage.frame = CGRect(x: 3, y: 0, width: self.articleImageWidth, height: self.articleImageHeight)
                self.bordureTop.hidden = true
                currentY += self.articleImageHeight
            }

            // Padding top
            currentY += 15

            // Surtitre
            if let surtitre = article.surtitre {
                self.surtitre.frame = CGRect(x: labelX, y: currentY, width: labelWidth, height: 0)
                self.surtitre.preferredMaxLayoutWidth = self.surtitre.frame.width
                self.surtitre.setTextWithLineHeight(surtitre.uppercaseString, lineHeight: 3)
                self.surtitre.sizeToFit()

                currentY += self.surtitre.frame.height
                currentY += 15
            } else {
                self.surtitre.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
            }

            // Titre
            self.titre.frame = CGRect(x: labelX, y: currentY, width: labelWidth, height: 0)
            self.titre.preferredMaxLayoutWidth = self.titre.frame.width
            self.titre.setTextWithLineHeight(article.titre, lineHeight: 3)
            self.titre.sizeToFit()

            currentY += self.titre.frame.height

            // Amorce
            if let amorce = article.amorce {
                currentY += 15

                self.amorce.frame = CGRect(x: labelX, y: currentY, width: labelWidth, height: 0)
                self.amorce.preferredMaxLayoutWidth = self.amorce.frame.width
                self.amorce.setTextWithLineHeight(amorce, lineHeight: 3)
                self.amorce.sizeToFit()

                currentY += self.amorce.frame.height
            } else {
                self.amorce.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
            }

            // Boutons
            currentY += 9

            self.updateButtonsPosition(currentY)
            self.layoutUpdatedAt(currentY)
            currentY += self.favorisButton.frame.height

            // Padding bottom
            currentY += 15

            // Couleurs
            self.bordureLeft.frame = CGRect(x: 0, y: 0, width: 3, height: currentY - 2)
            if let section = article.sectionSource {
                if let couleurFoncee = section.couleurFoncee {
                    self.bordureLeft.backgroundColor = couleurFoncee
                    self.surtitre.textColor = couleurFoncee
                }
            }

            // Mettre à jour le frame du contentView avec la bonne hauteur totale
            var frame = self.contentView.frame
            frame.size.height = currentY
            self.contentView.frame = frame
        }

    }

}

还有基类:

//
//  BaseArticleTableViewCell.swift
//

import UIKit

class BaseArticleTableViewCell: UITableViewCell {

    var backgroundThread: NSURLSessionDataTask?
    var delegate: SectionViewController?

    var favorisButton: FavorisButton!
    var shareButton: ShareButton!

    var updatedAt: UILabel!

    var fullWidth = CGFloat(0)

    var article: Article? {
        didSet {
            // Update du UI quand on set l'article
            updateArticle()
        }
    }

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        self.selectionStyle = UITableViewCellSelectionStyle.None
        self.contentView.backgroundColor = UIColor(colorLiteralRed: 248/255, green: 248/255, blue: 248/255, alpha: 1.0)

        // Largeur de la cellule, qui est toujours plein écran dans notre cas
        // self.contentView.frame.width ne donne pas la bonne valeur tant que le tableView n'a pas été layouté
        fullWidth = UIScreen.mainScreen().bounds.width

        self.favorisButton = FavorisButton(frame: CGRect(x: fullWidth - 40, y: 0, width: 28, height: 30))
        self.shareButton = ShareButton(frame: CGRect(x: fullWidth - 73, y: 0, width: 28, height: 30))

        self.updatedAt = UILabel(frame: CGRect(x: 18, y: 0, width: 0, height: 0))
        self.updatedAt.font = UIFont(name: "Graphik-Regular", size: 10)
        self.updatedAt.textColor = UIColor(colorLiteralRed: 138/255, green: 138/255, blue: 138/255, alpha: 1.0)
        self.updatedAt.backgroundColor = self.contentView.backgroundColor

        self.addSubview(self.favorisButton)
        self.addSubview(self.shareButton)
        self.addSubview(self.updatedAt)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // Avant qu'une cell soit réutilisée, faire un cleanup
    override func prepareForReuse() {
        super.prepareForReuse()

        // Canceller un background thread si y'en a un actif
        if let backgroundThread = self.backgroundThread {
            backgroundThread.cancel()
            self.backgroundThread = nil
        }

        resetUI()
    }

    // Updater le UI
    func updateArticle() {
        self.favorisButton.article = article
        self.shareButton.article = article

        if let delegate = self.delegate {
            self.shareButton.delegate = delegate
        }
    }

    // Faire un reset du UI avant de réutiliser une instance de Cell
    func resetUI() {

    }

    // Mettre à jour la position des boutons
    func updateButtonsPosition(currentY: CGFloat) {

        // Déjà positionnés en X, width, height, reste le Y
        var shareFrame = self.shareButton.frame
        shareFrame.origin.y = currentY
        self.shareButton.frame = shareFrame

        var favorisFrame = self.favorisButton.frame
        favorisFrame.origin.y = currentY + 1
        self.favorisButton.frame = favorisFrame
    }

    // Mettre à jour la position du updatedAt et son texte
    func layoutUpdatedAt(currentY: CGFloat) {
        var frame = self.updatedAt.frame
        frame.origin.y = currentY + 15
        self.updatedAt.frame = frame

        if let updatedAt = article?.updatedAtListe {
            self.updatedAt.text = updatedAt
        } else {
            self.updatedAt.text = ""
        }

        self.updatedAt.sizeToFit()
    }

}

在我的 ViewController 的 viewDidLoad 上,我预先计算了所有的行高:

// Créer une cache des row height des articles
func calculRowHeight() {
    self.articleRowHeights = [Int: CGFloat]()

    // Utiliser une seule instance de chaque type de cell
    let articleCell = tableView.dequeueReusableCellWithIdentifier("articleCell") as! BaseArticleTableViewCell
    let chroniqueCell = tableView.dequeueReusableCellWithIdentifier("chroniqueCell") as! BaseArticleTableViewCell

    var cell: BaseArticleTableViewCell!

    for articleObj in section.articles {
        let article = articleObj as! Article

        // Utiliser le bon type de cell
        if article.type == TypeArticle.Article {
            cell = articleCell
        } else {
            cell = chroniqueCell
        }

        // Setter l'article et refaire le layout
        cell.article = article
        cell.layoutSubviews()

        // Prendre la hauteur générée
        self.articleRowHeights[article.id] = cell.contentView.frame.height
    }
}

为请求的单元格设置行高:

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    let tableViewSection = tableViewSectionAtIndex(indexPath.section)
    let tableViewRow = tableViewSection.rows.objectAtIndex(indexPath.row) as! TableViewRow

    switch tableViewRow.type! {

    case TableViewRowType.Article :
        let article = tableViewRow.article!
        return self.articleRowHeights[article.id]!

    default:
        return UITableViewAutomaticDimension
    }
}

返回cellForRowAtIndexPath 中的单元格(我的tableView 中有多种单元格类型,因此需要进行一些检查):

// Cellule pour un section/row spécifique
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let tableViewSection = tableViewSectionAtIndex(indexPath.section)
    let tableViewRow = tableViewSection.rows.objectAtIndex(indexPath.row) as! TableViewRow

    switch tableViewRow.type! {

    case TableViewRowType.Article :
        let article = tableViewRow.article!

        if article.type == TypeArticle.Article {
            let cell = tableView.dequeueReusableCellWithIdentifier("articleCell", forIndexPath: indexPath) as! ArticleTableViewCell
            cell.delegate = self
            cell.article = article

            if let imageView = articleImageCache[article.id] {
                cell.articleImage.image = imageView
                cell.shareButton.image = imageView
            } else {
                cell.articleImage.image = placeholder
                loadArticleImage(article, articleCell: cell)
            }

            return cell

        }

        return UITableViewCell()
    default:
        return UITableViewCell()

    }
}

【讨论】:

  • 您好,我也有类似的问题,即使使用自动布局、图像背景加载和压缩,滚动也不流畅。但是如果你预先计算了所有的单元格,并且如果有很多单元格(比如数千个),那么表格的初始加载会不会很慢?
  • 在这种情况下,我会预先计算当前显示单元格的下 10 个单元格。
  • 但是一旦你计算出接下来的 10 个单元格,这不会影响垂直滚动条的位置吗?我的意思是这可能会再次引起抽搐?
  • 我不认为它会混蛋。您必须为每个单元格提供估计的行高,您可以返回一个平均值。
  • 我尝试预先计算 3000 个单元格,到目前为止,滞后并不明显。所以,我会先尝试这个策略。
猜你喜欢
  • 2016-06-11
  • 2015-06-27
  • 2015-10-27
  • 1970-01-01
  • 2019-12-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多