在 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()
添加到viewDidAppear
和viewWillAppear
当然可以。但是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: invalidateLayout
在 viewDidLoad
中异步。
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。 invalidateLayout
在viewWillLayoutSubviews
。
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 中多个设备之间的差异,需要调用 layoutIfNeeded
和 invalidateLayout
。该解决方案基于 @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.swift
中CollectionViewCell
子类的先前实现:
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”
仅在 iPhone 13 Pro 的中间不显示集合视图单元格