UICollectionView,全宽单元格,允许自动布局动态高度? [英] UICollectionView, full width cells, allow autolayout dynamic height?
问题描述
注意 2021 年!请参阅@Ely 关于 UICollectionLayoutListConfiguration
的回答 !!!!
在一个垂直的 UICollectionView
,
是否可以有全宽单元格,但允许动态高度由autolayout控制?
这可能是iOS 中最重要的问题,但没有真正好的答案".
重要:
请注意,在 99% 的情况下,要实现全宽单元格 + 自动布局动态高度,只需使用表格视图.就是这么简单.
那么你需要一个集合视图的例子是什么?
集合视图比表格视图强大得多.
一个简单的示例,您必须使用具有自动布局动态高度的集合视图:
如果您在集合视图中的两个布局之间动画.例如,在 1 到 2 列布局之间,当设备旋转时.
这是 iOS 中的常见习语.不幸的是,它只能通过解决此 QA 中提出的问题来实现.:-/
1.iOS 13+ 解决方案
在 Swift 5.1 和 iOS 13 中,您可以使用
<小时>2.iOS 11+ 解决方案
使用 Swift 5.1 和 iOS 11,您可以继承 UICollectionViewFlowLayout
并将其 estimatedItemSize
属性设置为 UICollectionViewFlowLayout.automaticSize
(这会告诉系统你想处理自动调整 UICollectionViewCell
s).然后,您必须覆盖 layoutAttributesForElements(in:)
和 layoutAttributesForItem(at:)
以设置单元格宽度.最后,您必须重写单元格的 preferredLayoutAttributesFitting(_:)
方法并计算其高度.
下面的完整代码展示了如何在全宽UIcollectionViewCell
内显示多行UICollectionViewCell
(受UICollectionView
的安全区域和UICollectionViewFlowLayout
的插图):
CollectionViewController.swift
导入 UIKit类 CollectionViewController: UICollectionViewController {让项目= [["Lorem ipsum dolor sit amet.",Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.Lorem ipsum dolor sit amet, consectetur adipiscing elit.","Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",],["Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incidudunt.","Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",],["Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incidudunt.","Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",Lorem ipsum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",]]让 customFlowLayout = CustomFlowLayout()覆盖 func viewDidLoad() {super.viewDidLoad()customFlowLayout.sectionInsetReference = .fromContentInset//.fromContentInset 是默认值customFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSizecustomFlowLayout.minimumInteritemSpacing = 10customFlowLayout.minimumLineSpacing = 10customFlowLayout.sectionInset = UIEdgeInsets(上:10,左:10,下:10,右:10)customFlowLayout.headerReferenceSize = CGSize(宽度:0,高度:40)collectionView.collectionViewLayout = customFlowLayoutcollectionView.contentInsetAdjustmentBehavior = .alwayscollectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")}覆盖func numberOfSections(在collectionView中:UICollectionView)->诠释{返回 items.count}覆盖 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) ->诠释{返回项目[section].count}覆盖 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) ->UICollectionViewCell {让 cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as!自定义单元cell.label.text = items[indexPath.section][indexPath.row]返回单元格}覆盖 func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) ->UICollectionReusableView {让 headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as!页眉视图headerView.label.text = "标题"返回标题视图}}
CustomFlowLayout.swift
导入 UIKit最终类 CustomFlowLayout: UICollectionViewFlowLayout {覆盖 func layoutAttributesForElements(in rect: CGRect) ->[UICollectionViewLayoutAttributes]?{让 layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as?[UICollectionViewLayoutAttributes]layoutAttributesObjects?.forEach({ layoutAttributes in如果 layoutAttributes.representedElementCategory == .cell {如果让 newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {layoutAttributes.frame = newFrame}}})返回布局属性对象}覆盖 func layoutAttributesForItem(at indexPath: IndexPath) ->UICollectionViewLayoutAttributes?{守卫让collectionView = collectionView else {致命错误()}守卫让 layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as?UICollectionViewLayoutAttributes 其他 {返回零}layoutAttributes.frame.origin.x = sectionInset.leftlayoutAttributes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right返回布局属性}}
HeaderView.swift
导入 UIKit类 HeaderView: UICollectionReusableView {让标签 = UILabel()覆盖初始化(帧:CGRect){超级初始化(帧:帧)背景颜色 = .洋红色添加子视图(标签)label.translatesAutoresizingMaskIntoConstraints = falselabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = truelabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true}需要初始化?(编码器 aDecoder:NSCoder){fatalError("init(coder:) 尚未实现")}}
CustomCell.swift
导入 UIKit类 CustomCell:UICollectionViewCell {让标签 = UILabel()覆盖初始化(帧:CGRect){超级初始化(帧:帧)标签.numberOfLines = 0背景颜色 = .orangecontentView.addSubview(标签)label.translatesAutoresizingMaskIntoConstraints = falselabel.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = truelabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = truelabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = truelabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true}需要初始化?(编码器 aDecoder:NSCoder){fatalError("init(coder:) 尚未实现")}覆盖 func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) ->UICollectionViewLayoutAttributes {让 layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)布局如果需要()layoutAttributes.frame.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)返回布局属性}}
以下是 preferredLayoutAttributesFitting(_:)
的一些替代实现:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) ->UICollectionViewLayoutAttributes {让targetSize = CGSize(宽度:layoutAttributes.frame.width,高度:0)layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)返回布局属性}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) ->UICollectionViewLayoutAttributes {label.preferredMaxLayoutWidth = layoutAttributes.frame.widthlayoutAttributes.frame.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height返回布局属性}
预期显示:
Note For 2021! See @Ely answer regarding UICollectionLayoutListConfiguration
!!!!
In a vertical UICollectionView
,
Is it possible to have full-width cells, but, allow the dynamic height to be controlled by autolayout?
This strikes me as perhaps the "most important question in iOS with no really good answer."
Important:
Note that in 99% of cases, to achieve full width cells + autolayout dynamic height, simply use a table view. It's that easy.
So what's an example of where you need a collection view?
Collection views are far more powerful than table views.
One straightforward example where you must use a collection view with autolayout dynamic height:
If you animate between two layouts in a collection view. For example, between a 1 and 2 column layout, when the device rotates.
That's a common idiom in iOS. Unfortunately it can only be achieved by solving the problem posed in this QA. :-/
1. Solution for iOS 13+
With Swift 5.1 and iOS 13, you can use Compositional Layout objects in order to solve your problem.
The following complete sample code shows how to display multiline UILabel
inside full-width UICollectionViewCell
:
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let items = [
[
"Lorem ipsum dolor sit amet.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
],
[
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
],
[
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Lorem ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
]
]
override func viewDidLoad() {
super.viewDidLoad()
let size = NSCollectionLayoutSize(
widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
heightDimension: NSCollectionLayoutDimension.estimated(44)
)
let item = NSCollectionLayoutItem(layoutSize: size)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
section.interGroupSpacing = 10
let headerFooterSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(40)
)
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: headerFooterSize,
elementKind: "SectionHeaderElementKind",
alignment: .top
)
section.boundarySupplementaryItems = [sectionHeader]
let layout = UICollectionViewCompositionalLayout(section: section)
collectionView.collectionViewLayout = layout
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items[section].count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.label.text = items[indexPath.section][indexPath.row]
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView
headerView.label.text = "Header"
return headerView
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { context in
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
}
}
HeaderView.swift
import UIKit
class HeaderView: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .magenta
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
CustomCell.swift
import UIKit
class CustomCell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.numberOfLines = 0
backgroundColor = .orange
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Expected display:
2. Solution for iOS 11+
With Swift 5.1 and iOS 11, you can subclass UICollectionViewFlowLayout
and set its estimatedItemSize
property to UICollectionViewFlowLayout.automaticSize
(this tells the system that you want to deal with autoresizing UICollectionViewCell
s). You'll then have to override layoutAttributesForElements(in:)
and layoutAttributesForItem(at:)
in order to set cells width. Lastly, you'll have to override your cell's preferredLayoutAttributesFitting(_:)
method and compute its height.
The following complete code shows how to display multiline UILabel
inside full-width UIcollectionViewCell
(constrained by UICollectionView
's safe area and UICollectionViewFlowLayout
's insets):
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let items = [
[
"Lorem ipsum dolor sit amet.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
],
[
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
],
[
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Lorem ipsum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
]
]
let customFlowLayout = CustomFlowLayout()
override func viewDidLoad() {
super.viewDidLoad()
customFlowLayout.sectionInsetReference = .fromContentInset // .fromContentInset is default
customFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
customFlowLayout.minimumInteritemSpacing = 10
customFlowLayout.minimumLineSpacing = 10
customFlowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
customFlowLayout.headerReferenceSize = CGSize(width: 0, height: 40)
collectionView.collectionViewLayout = customFlowLayout
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items[section].count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.label.text = items[indexPath.section][indexPath.row]
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderView
headerView.label.text = "Header"
return headerView
}
}
CustomFlowLayout.swift
import UIKit
final class CustomFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]
layoutAttributesObjects?.forEach({ layoutAttributes in
if layoutAttributes.representedElementCategory == .cell {
if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {
layoutAttributes.frame = newFrame
}
}
})
return layoutAttributesObjects
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let collectionView = collectionView else {
fatalError()
}
guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
return nil
}
layoutAttributes.frame.origin.x = sectionInset.left
layoutAttributes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
return layoutAttributes
}
}
HeaderView.swift
import UIKit
class HeaderView: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .magenta
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
CustomCell.swift
import UIKit
class CustomCell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
label.numberOfLines = 0
backgroundColor = .orange
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
layoutIfNeeded()
layoutAttributes.frame.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return layoutAttributes
}
}
Here are some alternative implementations for preferredLayoutAttributesFitting(_:)
:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return layoutAttributes
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
label.preferredMaxLayoutWidth = layoutAttributes.frame.width
layoutAttributes.frame.size.height = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
return layoutAttributes
}
Expected display:
这篇关于UICollectionView,全宽单元格,允许自动布局动态高度?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!