在 UICollectionView 中自动调整单元格(全部在代码中)
Posted
技术标签:
【中文标题】在 UICollectionView 中自动调整单元格(全部在代码中)【英文标题】:Autosizing Cells in UICollectionView (all in code) 【发布时间】:2019-04-14 13:37:20 【问题描述】:使用 Swift-5.0、Xcode-10.2、ios-12.2,
在尝试实现 UICollectionView 中单元格的“自动调整大小”(使用 UICollectionViewFlowLayout 作为其布局)时,我的代码中存在问题。
单元格的高度是基于内容的(并且预先未知) - 因此我尝试让 单元格的高度自动调整大小(我现在不关心的“宽度”可以,例如,设置为frame.width
)。
即使很难,我在下面的例子中只使用了一个大的 UICollectionView-Cell,我仍然想保持单元格的“出队”,因为以后会有更多的单元格需要填充大量内容.因此,为了使这个示例更简单,我将numberOfItemsInSection
保留为 1。
此外,对于下面的示例,每个自定义 CollectionViewCell 都填充有一个垂直 StackView(垂直添加了几个 Labels 和 ImageViews)。 StackView 在这里并不重要,但我把它作为一个简单的例子。同样,自定义 CollectionViewCell 的内容稍后会更改(特别是它将更改为依赖于内容的高度)。下面代码中固定高度的单元格内容只是示例原因...
但我仍然想摆脱这个问题,是关于如何使 CollectionViewCell 根据内容自动调整其高度的问题(无论是像下面的示例中的固定高度还是像在未来的实施)???
我不断收到以下约束错误:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
如果我在我的UICollectionViewController
中使用sizeForItemAt
方法给单元格一个非常大的固定高度,那么一切都有效。 (即“非常大”我的意思是比单元格出列时单元格的高度大得多)。
但如上所述,我不想将单元格的高度设置为固定值(使用 UICollectionViewController 的方法 sizeForItemAt
- 而是实现所需的自动调整大小。
要自动调整单元格大小,我尝试了以下方法:
方法 A)
使用collectionViewFlowLayout.estimatedItemSize = CGSize(...)
(并且不要使用sizeForItemAt
方法)
--> 同样的事情:如果估计的大小设置得足够大,则不会发生错误。如果我将它设置得太小,那么我会得到相同的约束错误...
方法 B)
使用collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
(使用或不使用sizeForItemAt
方法 - 没有区别)
--> 同样的错误:一旦我开始在 UICollectionView-cell 上滚动,它就会抛出相同的 Constraint-error...
观察到使用UICollectionViewFlowLayout.automaticSize
使应用程序按预期工作(除了上述错误仍然被抛出 - 但很奇怪,应用程序仍然以某种方式继续运行)这一观察结果颇有希望。对我来说,应用程序正常工作并引发错误是不可接受的。问题是,如何摆脱约束错误??
这是我使用的所有代码:
class TestViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout
let cellId = "cellID"
override func viewDidLoad()
super.viewDidLoad()
collectionView.backgroundColor = .yellow
collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
return 1
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell
return cell
// func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
// return .init(width: view.frame.width, height: 4000)
//
init()
let collectionViewFlowLayout = UICollectionViewFlowLayout()
// collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 4000)
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
super.init(collectionViewLayout: collectionViewFlowLayout)
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
这里是 CustomCollectionViewCell:
import UIKit
class MyCollectionViewCell: UICollectionViewCell
let titleLabel1 = UILabel(text: "Title 123", font: .boldSystemFont(ofSize: 30))
let titleLabel2 = UILabel(text: "Testing...", font: .boldSystemFont(ofSize: 15))
let imgView1 = UIImageView(image: nil)
let imgView2 = UIImageView(image: nil)
let imgView3 = UIImageView(image: nil)
let imgView4 = UIImageView(image: nil)
let imgView5 = UIImageView(image: nil)
let imageView: UIImageView =
let imgView = UIImageView()
imgView.backgroundColor = .green
return imgView
()
override init(frame: CGRect)
super.init(frame: frame)
backgroundColor = .yellow
titleLabel1.constrainHeight(constant: 50)
titleLabel1.constrainWidth(constant: 250)
titleLabel2.constrainHeight(constant: 50)
titleLabel2.constrainWidth(constant: 250)
imgView1.constrainHeight(constant: 100)
imgView1.constrainWidth(constant: 200)
imgView1.backgroundColor = .green
imgView2.constrainHeight(constant: 100)
imgView2.constrainWidth(constant: 200)
imgView2.backgroundColor = .green
imgView3.constrainHeight(constant: 100)
imgView3.constrainWidth(constant: 200)
imgView3.backgroundColor = .green
imgView4.constrainHeight(constant: 100)
imgView4.constrainWidth(constant: 200)
imgView4.backgroundColor = .green
imgView5.constrainHeight(constant: 100)
imgView5.constrainWidth(constant: 200)
imgView5.backgroundColor = .green
let stackView = UIStackView(arrangedSubviews: [titleLabel1, imgView1, titleLabel2, imgView2, imgView3, imgView4, imgView5])
addSubview(stackView)
stackView.anchor(top: safeAreaLayoutGuide.topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor)
stackView.spacing = 20
stackView.axis = .vertical
stackView.alignment = .center
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
需要的扩展定义如下:
extension UILabel
convenience init(text: String, font: UIFont)
self.init(frame: .zero)
self.text = text
self.font = font
self.backgroundColor = .red
extension UIImageView
convenience init(cornerRadius: CGFloat)
self.init(image: nil)
self.layer.cornerRadius = cornerRadius
self.clipsToBounds = true
self.contentMode = .scaleAspectFill
为了抽象出单元格组件的自动布局约束的锚定和高度定义(即填充单元格的一堆标签和图像视图),我使用了以下 UIView 扩展...:
(扩展程序已由 Brian Voong 发布 - 请参阅 video link)
// Reference Video: https://youtu.be/iqpAP7s3b-8
extension UIView
@discardableResult
func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero) -> AnchoredConstraints
translatesAutoresizingMaskIntoConstraints = false
var anchoredConstraints = AnchoredConstraints()
if let top = top
anchoredConstraints.top = topAnchor.constraint(equalTo: top, constant: padding.top)
if let leading = leading
anchoredConstraints.leading = leadingAnchor.constraint(equalTo: leading, constant: padding.left)
if let bottom = bottom
anchoredConstraints.bottom = bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom)
if let trailing = trailing
anchoredConstraints.trailing = trailingAnchor.constraint(equalTo: trailing, constant: -padding.right)
if size.width != 0
anchoredConstraints.width = widthAnchor.constraint(equalToConstant: size.width)
if size.height != 0
anchoredConstraints.height = heightAnchor.constraint(equalToConstant: size.height)
[anchoredConstraints.top, anchoredConstraints.leading, anchoredConstraints.bottom, anchoredConstraints.trailing, anchoredConstraints.width, anchoredConstraints.height].forEach $0?.isActive = true
return anchoredConstraints
func fillSuperview(padding: UIEdgeInsets = .zero)
translatesAutoresizingMaskIntoConstraints = false
if let superviewTopAnchor = superview?.topAnchor
topAnchor.constraint(equalTo: superviewTopAnchor, constant: padding.top).isActive = true
if let superviewBottomAnchor = superview?.bottomAnchor
bottomAnchor.constraint(equalTo: superviewBottomAnchor, constant: -padding.bottom).isActive = true
if let superviewLeadingAnchor = superview?.leadingAnchor
leadingAnchor.constraint(equalTo: superviewLeadingAnchor, constant: padding.left).isActive = true
if let superviewTrailingAnchor = superview?.trailingAnchor
trailingAnchor.constraint(equalTo: superviewTrailingAnchor, constant: -padding.right).isActive = true
func centerInSuperview(size: CGSize = .zero)
translatesAutoresizingMaskIntoConstraints = false
if let superviewCenterXAnchor = superview?.centerXAnchor
centerXAnchor.constraint(equalTo: superviewCenterXAnchor).isActive = true
if let superviewCenterYAnchor = superview?.centerYAnchor
centerYAnchor.constraint(equalTo: superviewCenterYAnchor).isActive = true
if size.width != 0
widthAnchor.constraint(equalToConstant: size.width).isActive = true
if size.height != 0
heightAnchor.constraint(equalToConstant: size.height).isActive = true
func centerXInSuperview()
translatesAutoresizingMaskIntoConstraints = false
if let superViewCenterXAnchor = superview?.centerXAnchor
centerXAnchor.constraint(equalTo: superViewCenterXAnchor).isActive = true
func centerYInSuperview()
translatesAutoresizingMaskIntoConstraints = false
if let centerY = superview?.centerYAnchor
centerYAnchor.constraint(equalTo: centerY).isActive = true
func constrainWidth(constant: CGFloat)
translatesAutoresizingMaskIntoConstraints = false
widthAnchor.constraint(equalToConstant: constant).isActive = true
func constrainHeight(constant: CGFloat)
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: constant).isActive = true
【问题讨论】:
【参考方案1】:最后,经过一番挖掘,我找到了解决方案:
如果要自动调整自定义 UICollectionViewCell 的大小,有四件事很重要:
-
在创建您的 UICollectionViewController 实例时,请确保您传递了一个 FlowLayout 已将
estimatedItemSize
属性设置为一些有根据的猜测值(在我的例子中,高度 = 1 [因为未知])
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 1)
let vc = TestViewController(collectionViewLayout: collectionViewFlowLayout)
.
-
在您的自定义 UICollectionViewCell 中创建一个属性来跟踪单元格的高度
(即在您的 UICollectionViewCell 类中,您将始终知道单元格的高度是多少(即使稍后动态更改)。再次,我想摆脱我需要在 UICollectionViewController 的 @ 处定义此高度的事实987654323@出列单元格之前的方法)
-
不要忘记在您的自定义 UICollectionViewCell 中添加
preferredLayoutAttributesFitting
方法:
let myContentHeight = CGFloat(720)
// in my above code-example, the 720 are the sum of 2 x 50 (labels) plus 5 x 100 (imageViews) plus 6 x 20 (spacing)
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
setNeedsLayout()
layoutIfNeeded()
var newFrame = layoutAttributes.frame
// myContentHeight corresponds to the total height of your custom UICollectionViewCell
newFrame.size.height = ceil(myContentHeight)
layoutAttributes.frame = newFrame
return layoutAttributes
-
当然,每当您的自定义 CollectionViewCell 动态改变其高度时,您还需要再次更改
myContentHeight
属性...
(preferredLayoutAttributesFitting
的调用你不需要在代码中关心自己,这个方法会在用户进入视图、滚动或做其他任何事情时自动调用。操作系统或多或少地关心这一点。 ..)。
【讨论】:
在第 4 点,您写来更改 myContentHeight,但是在哪里?以及何时需要更新?以上是关于在 UICollectionView 中自动调整单元格(全部在代码中)的主要内容,如果未能解决你的问题,请参考以下文章
自动调整UICollectionView高度以调整其内容大小
设备旋转时如何自动调整 UICollectionView 的大小?
自动调整嵌入在 UITableView 中的 UICollectionView 单元格不起作用
我在自定义 TableViewCell 中有一个 UICollectionView,但我无法自动调整 CollectionView 的大小
UICollectionView 具有自动调整单元格(estimatedSize)和 sectionHeadersPinToVisibleBounds 的精神