在 iOS 12 中,UICollectionView 何时布局单元格,在 nib 中使用 autolayout

Posted

技术标签:

【中文标题】在 iOS 12 中,UICollectionView 何时布局单元格,在 nib 中使用 autolayout【英文标题】:In iOS 12, when does the UICollectionView layout cells, use autolayout in nib 【发布时间】:2018-12-24 19:29:54 【问题描述】:

同样的代码

collectionLayout.estimatedItemSize = CGSize(width: 50, height: 25)
collectionLayout.itemSize = UICollectionViewFlowLayoutAutomaticSize
collectionLayout.minimumInteritemSpacing = 10 

for _ in 0 ..< 1000 
    let length = Int(arc4random() % 8)
    let string = randomKeyByBitLength(length)
    array.append(string!)

collectionView.reloadData()

单元格约束:

当我在 ios 12 上运行它时,情况有所不同。 左边模拟器是iOS 11,右边是iOS 12:

但是,当我滚动它时,单元格的框架会正常。


重现问题的示例项目:https://github.com/Coeur/***51375566

【问题讨论】:

我无法重现该问题,因为 ios 11 和 ios 12 产生相同的结果。 @PranavKasetti 我使用给定的代码在 Xcode 10 beta 6 中轻松复制了它。为方便起见,我已链接到问题中的示例项目。 谁能确认,这已在 Xcode 12 iOS 14 中修复? ???至少对我来说,它现在似乎可以在没有任何破解的情况下工作。 ?????? 【参考方案1】:
class CollectionViewCell: UICollectionViewCell 
@IBOutlet weak var label: UILabel!

override func awakeFromNib() 
    super.awakeFromNib()

    contentView.translatesAutoresizingMaskIntoConstraints = false

    let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor)
    let rightConstraint = contentView.rightAnchor.constraint(equalTo: rightAnchor)
    let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor)
    let bottomConstraint = contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
    NSLayoutConstraint.activate([leftConstraint, rightConstraint, topConstraint, bottomConstraint])

我已经尝试过了,它对我有用。试试这个,如果您需要任何帮助,请告诉我。

【讨论】:

【参考方案2】:

试试这个

override func viewWillLayoutSubviews() 
    super.viewWillLayoutSubviews()
    
    DispatchQueue.main.async 
        self.collectionView.collectionViewLayout.invalidateLayout()
    

添加到viewDidAppearviewWillAppear 当然可以。但是viewDidAppear 会给用户带来一个小故障。

【讨论】:

【参考方案3】:

问题在于这里提出的特性——集合视图单元格根据它们在 UICollectionViewFlowLayout 中的内部约束来调整自身大小——不存在。它从未存在过。苹果声称它确实如此,但事实并非如此。自从引入集合视图并首次提出此声明以来,我每年都对此提出错误;而且我的错误报告从未关闭,因为错误是真实的。不存在自调整大小的集合视图单元格之类的东西。

也可以在这里查看我的回答:https://***.com/a/51585910/341994

在某些年里,尝试使用自定尺寸单元失败了。在其他年份,它不会崩溃,但会导致布局错误。但它不起作用

唯一的方法是实现委托方法sizeForItemAt 并提供大小你自己。您可以通过调用

轻松做到这一点
cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)

在您预先配置的模型单元上。那是运行时应该为你做的——但它没有。

所以这是我对原始问题的解决方案。我们生成了一个字符串大小对的数组(作为一个元组),而不是一个简单的字符串数组。那么:

override func collectionView(_ collectionView: UICollectionView, 
    cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! MyCell
        cell.label.text = self.array[indexPath.row].0
        return cell


func collectionView(_ collectionView: UICollectionView, 
    layout collectionViewLayout: UICollectionViewLayout, 
    sizeForItemAt indexPath: IndexPath) -> CGSize 
        return self.array[indexPath.row].1

【讨论】:

“预先配置好的单元格”是什么意思?您实际上是在哪里拨打cell.contentView.systemLayoutSizeFitting(:) 的电话? @d4Rk 显示比描述更容易,请参阅raw.githubusercontent.com/mattneub/… 好的,所以基本上你自己实例化一个单元格,然后在sizeForItemAt 中用那个indexPath 的数据配置它,然后让systemLayoutSizeFitting“做自动布局”并返回那个大小。由于这可能是正确的解决方案,还有很多工作要做,但似乎没有真正好的解决方案,只要苹果不解决这个问题。 感谢@matt 的修复和代码示例。我可以欺骗任何雷达 ID? @GuillaumeAlgis 我关于自动调整大小的单元格崩溃 (18523277) 的报告已于 2015 年作为副本关闭。我后来(在 2016 年)提交了一份报告,如果您尝试使用自动调整大小的单元格 (28044647),则布局会出现错误,并且仍然处于打开状态。【参考方案4】:

对于所有解决方案,请注意无需在viewDidLoad 中显式调用reloadData:它会自动发生。

解决方案 1

灵感来自 Samantha idea: invalidateLayoutviewDidLoad 中异步。

override func viewDidLoad() 
    super.viewDidLoad()

    //[...]

    for _ in 0 ..< 1000 
        array.append(randomKeyByBitLength(Int(arc4random_uniform(8)))!)
    

    DispatchQueue.main.async 
        self.collectionView.collectionViewLayout.invalidateLayout()
    

解决方案 2

(不完善,见DHennessy13改进)

基于Peter Lapisu answer。 invalidateLayoutviewWillLayoutSubviews

override func viewWillLayoutSubviews() 
    super.viewWillLayoutSubviews()
    collectionView.collectionViewLayout.invalidateLayout()

正如 DHennessy13 所指出的,当前使用 viewWillLayoutSubviews 的解决方案并不完美,因为它会在旋转屏幕时使布局无效。

您可以关注DHennessy13 improvement 了解此解决方案。

解决方案 3

基于Tyler Sheaffer answer、Shawn Aukstak port to Swift 和 Samantha 想法的组合。将您的 CollectionView 子类化以在 layoutSubviews 上执行 invalidateLayout

class AutoLayoutCollectionView: UICollectionView 

    private var shouldInvalidateLayout = false

    override func layoutSubviews() 
        super.layoutSubviews()
        if shouldInvalidateLayout 
            collectionViewLayout.invalidateLayout()
            shouldInvalidateLayout = false
        
    

    override func reloadData() 
        shouldInvalidateLayout = true
        super.reloadData()
    

这个解决方案很优雅,因为它不需要更改您的 ViewController 代码。我已经在这个示例项目 https://github.com/Coeur/***51375566/tree/AutoLayoutCollectionView 的分支 AutoLayoutCollectionView 上实现了它。

解决方案 4

重写 UICollectionViewCell 默认约束。见Larry answer。

解决方案 5

实现collectionView(_:layout:sizeForItemAt:) 并返回cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)。见matt answer。

【讨论】:

第三种解决方案在iOS 12 - Xcode 10 上也有布局警告 尝试通过添加或删除来更改 collectionView 项目编号后。 @Stan 您可以提供一个关于哪个解决方案与集合视图编辑兼容的答案,并最终提供一个说明编辑问题的实现示例(可能是我的 github 的一个分支?) 第一个解决方案对我有用,但我不知道出了什么问题,因为没有那条线我已经在其他视图控制器中做到了,唯一的区别是在这个视图控制器中我有超过 1 个集合视图 解决方案 3 与拉里在 iOS 12.2 中使用 xib 文件对我的回答工作相结合。如果在故事板中使用集合视图,解决方案 3 就足够了【参考方案5】:

我们的项目也遇到了同样的问题。我们还注意到 iOS 12 中多个设备之间的差异,需要调用 layoutIfNeededinvalidateLayout。该解决方案基于 @DHennessy13 的方法,但不需要布尔值来管理看起来有点老套的状态。

这里是基于一个 Rx 代码,基本上第一行是当数据发生变化时,subscribe 里面是修复讨厌的 iOS 12 UI 故障需要做的:

        viewModel.cellModels.asObservable()
            .subscribe(onNext:  [weak self] _ in
                // iOS 12 bug in UICollectionView for cell size
                self?.collectionView.layoutIfNeeded()

                // required for iPhone 8 iOS 12 bug cell size
                self?.collectionView.collectionViewLayout.invalidateLayout()
            )
            .disposed(by: rx.disposeBag)

编辑:

顺便说一句,这似乎是 iOS 12 中的一个已知问题: https://developer.apple.com/documentation/ios_release_notes/ios_12_release_notes(在 UIKit 部分)。

【讨论】:

用RxSwift 替换布尔值并不是一种简化。您链接到的问题有一个官方建议,只需添加一个电话updateConstraintsIfNeeded() 我不会用 RxSwift 替换 Bool,不需要锤子机来做简单的钉子。这篇文章说明了如何通过 RxSwift 来做到这一点,因为我在我的项目中使用了这种方法,这也带来了在这种情况下防止 hacky Bool 技巧的优势。将其视为帮助使用 Rx 的人们回答最初问题的另一种解决方案。 // 另外,如果你仔细阅读,我只是指出 Apple 已经意识到了这个问题,我并不是在建议他们的官方建议对我来说无论如何都不起作用(我想你也不适合,因为你想出了解决问题的不同方法)。 我的应用中没有动态大小的集合视图,所以我还没有探索updateConstraintsIfNeeded:我的回答只是为了提供帮助。 :) 好吧,很高兴你提出了一个 RxSwift 解决方案。 哦,我的错,我以为你确实有动态尺寸。谢谢你的消息:)【参考方案6】:

这是另一个适用于Cœur 的代码示例的解决方案,也适用于我的特殊情况,而其他答案没有。下面的代码替换了ViewController.swiftCollectionViewCell子类的先前实现:

class CollectionViewCell: UICollectionViewCell 
    @IBOutlet weak var label: UILabel!

    override func awakeFromNib() 
        super.awakeFromNib()

        contentView.translatesAutoresizingMaskIntoConstraints = false

        let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor)
        let rightConstraint = contentView.rightAnchor.constraint(equalTo: rightAnchor)
        let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor)
        let bottomConstraint = contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
        NSLayoutConstraint.activate([leftConstraint, rightConstraint, topConstraint, bottomConstraint])
    

这是受到ale84 来自UICollectionViewFlowLayout estimatedItemSize does not work properly with iOS12 though it works fine with iOS 11.* 的回答的启发

【讨论】:

这种方法在inserting/deleting项目collectionView事件上仍然有布局警告:( 这种方法完全解决了我的问题。谢谢。 优秀。其他解决方案对我不起作用,但确实如此。 精彩,如此简单。绝对应该是公认的答案。 @OliverZhang 请与我分享一个小示例项目,其中一些其他解决方案不起作用,以便我分析是什么原因。谢谢。【参考方案7】:

Cœur 的解决方案 2 可防止布局在用户面前闪烁或更新。但是,当您旋转设备时,它可能会产生问题。我在 viewWillLayoutSubviews 中使用变量“shouldInvalidateLayout”,并在 viewDidAppear 中将其设置为 false。

private var shouldInvalidateLayout = true

override func viewDidAppear(_ animated: Bool) 
    super.viewDidAppear(animated)

    shouldInvalidateLayout = false


override func viewWillLayoutSubviews() 
    super.viewWillLayoutSubviews()
    if shouldInvalidateLayout 
        collectionView.collectionViewLayout.invalidateLayout()
    

【讨论】:

这是唯一一个为我工作的不眨眼的。没有Apple的任何解释,这怎么可能改变?他们只会说看看文档,在下一个版本中会出现另一个没有任何意义的错误。 @Andreas777 请与我分享一个小示例项目,其中一些其他解决方案不起作用,以便我分析原因。谢谢。【参考方案8】:

我有同样的问题,单元格使用估计大小(而不是自动大小)直到滚动。使用 Xcode 9.x 构建的相同代码在 iOS 11 和 12 上运行得非常好,而在 Xcode 10 中构建的代码在 iOS 11 上可以正常运行,但在 iOS 12 上无法正常运行。

到目前为止,我发现解决此问题的唯一方法是使集合视图的布局在 viewDidAppear 中无效,这可能会导致一些跳跃,或者在 viewWillAppear 内的异步块中(不确定该解决方案的可靠性如何)是)。

override func viewDidLoad() 
    super.viewDidLoad()
    let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    layout?.estimatedItemSize = CGSize(width: 50, height: 50)
    layout?.itemSize = UICollectionViewFlowLayout.automaticSize


override func viewWillAppear(_ animated: Bool) 
    super.viewWillAppear(animated)
    // The following block also "fixes" the problem without jumpiness after the view has already appeared on screen.
    DispatchQueue.main.async 
        self.collectionView.collectionViewLayout.invalidateLayout()
    


override func viewDidAppear(_ animated: Bool) 
    super.viewDidAppear(animated)
    // The following line makes cells size properly in iOS 12.
    collectionView.collectionViewLayout.invalidateLayout()

【讨论】:

它有效,谢谢。并在 viewWillAppear 中编写一个异步块看起来比 viewDidApear 更好。 我找到了替代方法来调用invalidateLayout 而不是viewWillAppear 我不得不将它与手动 NSString Size 调用结合起来,基本上只是手动返回大小。但是在按照您所说的进行操作之前,不会调用这些函数。 “sizeforitematindexpath”在 iOS 12 中基本上变成了 0,即使有这个答案

以上是关于在 iOS 12 中,UICollectionView 何时布局单元格,在 nib 中使用 autolayout的主要内容,如果未能解决你的问题,请参考以下文章

在单个 UICollectionViewController 中管理多个 UICollectionViewLayout

ios 8.1:类型“ViewController”不符合协议“UICollectionViewDataSource”

从核心数据加载 UICollectionView

仅在 iPhone 13 Pro 的中间不显示集合视图单元格

猫猫学iOS 之微博项目实战程序启动新特性用UICollectionViewController实现

UICollectionView - 如何在同一部分显示不同高度的项目?