注意 2021 年!请参阅关于 UICollectionLayoutListConfiguration
的@Ely 答案!!!!
在垂直 UICollectionView
中,
是否可以有全角单元格,但是,允许动态高度由自动布局控制?
这让我觉得这可能是“iOS 中最重要的问题,但没有真正好的答案”。
重要的:
请注意,在 99% 的情况下,要实现全宽单元格 + 自动布局动态高度,只需使用表格视图。就这么容易。
那么你需要一个集合视图的例子是什么?
集合视图比表视图强大得多。
一个简单的示例,您必须使用具有自动布局动态高度的集合视图:
如果您在集合视图中的两个布局之间进行动画处理。例如,在 1 和 2 列布局之间,当设备旋转时。
这是iOS中的一个常见习语。不幸的是,它只能通过解决此 QA 中提出的问题来实现。 :-/
1. iOS 13+ 解决方案
在 Swift 5.1 和 iOS 13 中,您可以使用 Compositional Layout objects 来解决您的问题。
以下完整示例代码显示了如何在全角 UICollectionViewCell
内显示多行 UILabel
:
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")
}
}
预期显示:
https://i.stack.imgur.com/gaS2r.png
2. iOS 11+ 解决方案
在 Swift 5.1 和 iOS 11 中,您可以继承 UICollectionViewFlowLayout
并将其 estimatedItemSize
属性设置为 UICollectionViewFlowLayout.automaticSize
(这告诉系统您要处理自动调整 UICollectionViewCell
)。然后,您必须覆盖 layoutAttributesForElements(in:)
和 layoutAttributesForItem(at:)
才能设置单元格宽度。最后,您必须覆盖单元格的 preferredLayoutAttributesFitting(_:)
方法并计算其高度。
以下完整代码显示了如何在全角 UIcollectionViewCell
内显示多行 UILabel
(受 UICollectionView
的安全区域和 UICollectionViewFlowLayout
的插入限制):
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
}
}
以下是 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
}
预期显示:
https://i.stack.imgur.com/o6XMi.png
问题
您正在寻找自动高度并且还希望有完整的宽度,使用 UICollectionViewFlowLayoutAutomaticSize
是不可能的。
您想使用 UICollectionView
,因此以下是适合您的解决方案。
解决方案
Step-I: 计算 Cell 的期望高度
1. 如果您在 CollectionViewCell
中只有 UILabel
而不是设置 numberOfLines=0
并且计算了 UIlable
的预期高度,则传递所有三个参数
func heightForLable(text:String, font:UIFont, width:CGFloat) -> CGFloat {
// pass string, font, LableWidth
let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height
}
2. 如果您的 CollectionViewCell
仅包含 UIImageView
并且它应该是动态的,那么您需要获得 UIImage
的高度 (您的 UIImageView
必须具有AspectRatio
个约束)
// this will give you the height of your Image
let heightInPoints = image.size.height
let heightInPixels = heightInPoints * image.scale
3.如果它包含两者而不是计算它们的高度并将它们加在一起。
STEP-II: 返回 CollectionViewCell 的大小
1. 在您的 viewController 中添加 UICollectionViewDelegateFlowLayout
委托
2.实现委托方法
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// This is just for example, for the scenario Step-I -> 1
let yourWidthOfLable=self.view.size.width
let font = UIFont(name: "Helvetica", size: 20.0)
var expectedHeight = heightForLable(array[indePath.row], font: font, width:yourWidthOfLable)
return CGSize(width: view.frame.width, height: expectedHeight)
}
我希望这会帮助你。
return CGSize(width: view.frame.width/2, height: expectedHeight)
还要考虑 Padding 而不是返回宽度,否则两个单元格将不适合单行。
有几种方法可以解决这个问题。
一种方法是您可以为集合视图流布局提供估计大小并使用 systemLayoutSizeFitting
计算单元大小。
注意:正如下面评论中提到的,从 iOS 10 开始,您不再需要提供估计大小来触发对单元格上的调用 func preferredLayoutAttributesFitting(_ layoutAttributes:)
。如果您希望调用 preferredLayoutAttributes,以前 (iOS 9) 会要求您提供估计大小。
(假设您正在使用故事板并且集合视图通过 IB 连接)
override func viewDidLoad() {
super.viewDidLoad()
let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
layout?.estimatedItemSize = CGSize(width: 375, height: 200) // your average cell size
}
对于简单的单元格,通常就足够了。如果大小仍然不正确,您可以在集合视图单元格中覆盖
func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
,这将使您能够更精细地控制单元格大小。 注意:您仍然需要为流布局提供估计大小。
然后覆盖 func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
以返回正确的大小。
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
或者,您可以改为使用大小调整单元格来计算 UICollectionViewDelegateFlowLayout
中单元格的大小。如果您使用此方法,请考虑缓存大小以提高性能。
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width
let size = CGSize(width: width, height: 0)
// assuming your collection view cell is a nib
// you may also instantiate an instance of your cell if it doesn't use a Nib
// let sizingCell = MyCollectionViewCell()
let sizingCell = UINib(nibName: "yourNibName", bundle: nil).instantiate(withOwner: nil, options: nil).first as! YourCollectionViewCell
sizingCell.autoresizingMask = [.flexibleWidth, .flexibleHeight]
sizingCell.frame.size = size
sizingCell.configure(with: object[indexPath.row]) // what ever method configures your cell
return sizingCell.contentView.systemLayoutSizeFitting(size, withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityDefaultLow)
}
虽然这些不是完美的生产就绪示例,但它们应该让您朝着正确的方向开始。我不能说这是最佳实践,但这对我有用,即使包含多个标签的相当复杂的单元格,可能会或可能不会换行。
preferredLayoutAttributesFitting
中的设置约束。然后我在所述函数中添加了我自己的约束,绑定到来自估计项大小的固定维度,FWIW,请参阅下面的答案。
我为这个问题找到了一个非常简单的解决方案:在我的 CollectionViewCell 内部,我得到了一个 UIView(),它实际上只是一个背景。为了获得全宽,我只需设置以下锚点
bgView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width - 30).isActive = true // 30 is my added up left and right Inset
bgView.topAnchor.constraint(equalTo: topAnchor).isActive = true
bgView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
bgView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
bgView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
“魔法”发生在第一行。我将 widthAnchor 动态设置为屏幕的宽度。同样重要的是减去 CollectionView 的插图。否则单元格将不会显示。如果您不想拥有这样的背景视图,只需使其不可见即可。
FlowLayout 使用以下设置
layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
结果是具有动态高度的全宽大小的单元格。
https://i.stack.imgur.com/kS3aV.png
在职的!!!在 IOS:12.1 Swift 4.1 上测试
我有一个非常简单的解决方案,它可以在不破坏约束的情况下工作。
https://i.stack.imgur.com/bhXvg.jpg
我的视图控制器类
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
let cellId = "CustomCell"
var source = ["nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,"]
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.register(UINib.init(nibName: cellId, bundle: nil), forCellWithReuseIdentifier: cellId)
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.source.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as? CustomCell else { return UICollectionViewCell() }
cell.setData(data: source[indexPath.item])
return cell
}
}
自定义单元类:
class CustomCell: UICollectionViewCell {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var widthConstraint: NSLayoutConstraint!
override func awakeFromNib() {
super.awakeFromNib()
self.widthConstraint.constant = UIScreen.main.bounds.width
}
func setData(data: String) {
self.label.text = data
}
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return contentView.systemLayoutSizeFitting(CGSize(width: self.bounds.size.width, height: 1))
}
}
主要成分是 Customcell 中的 systemLayoutSizeFitting 函数。而且我们还必须在 Cell 内设置视图的宽度和约束。
preferredLayoutAttributesFitting
方法来实现。
如果您使用的是 iOS 14 或更新版本,那么您可以使用 UICollectionLayoutListConfiguration
API,它可以将 UICollectionView
用作单列表格视图,包括水平滑动上下文菜单和自动高度单元格。
var collectionView: UICollectionView! = nil
override func viewDidLoad() {
super.viewDidLoad()
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
}
有关如何配置单元格和数据源的更多信息(包括示例项目)可以在 Apple 的这篇文章中找到:Implementing Modern Collection Views,尤其是以创建简单列表布局开头的部分。
示例项目包含一个名为 ConferenceNewsFeedViewController 的控制器,它显示了如何基于自动布局配置自动高度单元格。
设置流布局的estimatedItemSize:collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize 在单元格中定义一个宽度约束,并将其设置为等于superview的宽度:class CollectionViewCell: UICollectionViewCell { private var widthConstraint: NSLayoutConstraint? ... override init(frame: CGRect) { ... // 创建宽度约束以便稍后设置。 widthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0) } override func updateConstraints() { // 将宽度约束设置为 superview 的宽度。 widthConstraint?.constant = superview?.bounds.width ?? 0 widthConstraint?.isActive = true super.updateConstraints() } ... }
在 iOS 11 上测试。
您必须向 CollectionViewCell 添加宽度约束
class SelfSizingCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
}
}
就个人而言,我发现拥有一个 UICollectionView 的最佳方法,其中 AutoLayout 确定大小,而每个 Cell 可以具有不同的大小,即在使用实际 Cell 测量大小的同时实现 UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath 函数。
我在 my blog posts 之一中谈到了这一点
希望这个能帮助你实现你想要的。我不是 100% 确定,但我相信与 UITableView 不同的是,您实际上可以通过使用 AutoLayout 结合使用来获得全自动的单元格高度
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
UICollectionView 没有这种让 AutoLayout 确定大小的方式,因为 UICollectionViewCell 不一定会填满屏幕的整个宽度。
但是这里有一个问题要问:如果您需要全屏宽度的单元格,为什么还要费心使用 UICollectionView 而不是带有自动调整大小的单元格的旧 UITableView?
根据我对 Eric 回答的评论,我的解决方案与他的非常相似,但我必须在 preferredSizeFor... 中添加一个约束以约束到固定尺寸。
override func systemLayoutSizeFitting(
_ targetSize: CGSize, withHorizontalFittingPriority
horizontalFittingPriority: UILayoutPriority,
verticalFittingPriority: UILayoutPriority) -> CGSize {
width.constant = targetSize.width
let size = contentView.systemLayoutSizeFitting(
CGSize(width: targetSize.width, height: 1),
withHorizontalFittingPriority: .required,
verticalFittingPriority: verticalFittingPriority)
print("\(#function) \(#line) \(targetSize) -> \(size)")
return size
}
此问题有多个重复项 I answered it in detail here,并提供了一个 working sample app here.
不确定这是否符合“非常好的答案”,但这是我用来完成此任务的。我的流布局是水平的,我正在尝试使用自动布局调整宽度,所以它与您的情况相似。
extension PhotoAlbumVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// My height is static, but it could use the screen size if you wanted
return CGSize(width: collectionView.frame.width - sectionInsets.left - sectionInsets.right, height: 60)
}
}
然后在自动布局约束被修改的视图控制器中,我触发了一个 NSNotification。
NotificationCenter.default.post(name: NSNotification.Name("constraintMoved"), object: self, userInfo: nil)
在我的 UICollectionView 子类中,我监听该通知:
// viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(handleConstraintNotification(notification:)), name: NSNotification.Name("constraintMoved"), object: nil)
并使布局无效:
func handleConstraintNotification(notification: Notification) {
self.collectionView?.collectionViewLayout.invalidateLayout()
}
这会导致使用集合视图的新大小再次调用 sizeForItemAt
。在您的情况下,它应该能够根据布局中可用的新约束进行更新。
在您的 viewDidLayoutSubviews
上,将 estimatedItemSize
设置为全宽(布局是指 UICollectionViewFlowLayout 对象):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.bounds.size.width, height: 120)
}
在您的单元格上,确保您的约束同时触及单元格的顶部和底部(以下代码使用制图来简化设置约束,但您可以根据需要使用 NSLayoutConstraint 或 IB 来完成):
constrain(self, nameLabel, valueLabel) { view, name, value in
name.top == view.top + 10
name.left == view.left
name.bottom == view.bottom - 10
value.right == view.right
value.centerY == view.centerY
}
瞧,你的细胞现在会自动长高!
AutoLayout 可用于通过 2 个简单的步骤自动调整 CollectionView 中的单元格大小:
启用动态单元格大小
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
拥有一个容器视图并从 collectionView(:cellForItemAt:) 中设置 containerView.widthAnchor.constraint 以将 contentView 的宽度限制为 collectionView 的宽度。
class ViewController: UIViewController, UICollectionViewDataSource {
...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! MultiLineCell
cell.textView.text = dummyTextMessages[indexPath.row]
cell.maxWidth = collectionView.frame.width
return cell
}
...
}
class MultiLineCell: UICollectionViewCell{
....
var maxWidth: CGFloat? {
didSet {
guard let maxWidth = maxWidth else {
return
}
containerViewWidthAnchor.constant = maxWidth
containerViewWidthAnchor.isActive = true
}
}
....
}
就是这样,你会得到想要的结果。有关完整代码,请参阅以下要点:
SelfSizingCollectionViewCellDemo+UILabel.swift
SelfSizingCollectionViewCellDemo+UITextView.swift
参考/学分:
V8tr 的博文 - Collection View Cells Self-Sizing: Step by Step Tutorial
另一个stackoverflow答案
https://i.stack.imgur.com/2W8CH.png
我还遇到了动态单元格的高度问题,可以使用自动布局和手动高度计算来解决这个问题。无需使用估计大小、实例化或创建单元格。
此解决方案还提供多行标签的处理。它基于计算单元格中所有子视图的子视图高度。
extension CollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let contentHorizontalSpaces = collectionLayout.minimumInteritemSpacing
+ collectionLayout.sectionInset.left
+ collectionLayout.sectionInset.right
let newCellWidth = (collectionView.bounds.width - contentHorizontalSpaces) / 2
let newHeight = Cell.getProductHeightForWidth(props: data[indexPath.row], width: newCellWidth)
return CGSize(width: newCellWidth, height: newHeight)
}
}
UICollectionViewDelegateFlowLayout 使用 getProductHeightForWidth
方法计算的单元格大小:
extension Cell {
class func getProductHeightForWidth(props: Props, width: CGFloat) -> CGFloat {
// magic numbers explanation:
// 16 - offset between image and price
// 22 - height of price
// 8 - offset between price and title
var resultingHeight: CGFloat = 16 + 22 + 8
// get image height based on width and aspect ratio
let imageHeight = width * 2 / 3
resultingHeight += imageHeight
let titleHeight = props.title.getHeight(
font: .systemFont(ofSize: 12), width: width
)
resultingHeight += titleHeight
return resultingHeight
}
}
我在这里创建了一个故事:https://volodymyrrykhva.medium.com/uicollectionview-cells-with-dynamic-height-using-autolayout-a4e346b7bd2a
解决方案的完整代码在 GitHub 上:https://github.com/ascentman/DynamicHeightCells
对于具有动态高度的单元格,我已经看到了许多复杂的答案。然而,经过几次谷歌搜索,我找到了这个简单的答案。
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UINib(nibName: "LabelOnlyCell", bundle: nil), forCellWithReuseIdentifier: "LabelOnlyCell")
if let collectionViewLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
重要部分
collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
LabelOnlyCell 类供参考
class LabelOnlyCell: UICollectionViewCell {
@IBOutlet weak var labelHeading: UILabel!
@IBOutlet weak var parentView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.parentView.translatesAutoresizingMaskIntoConstraints = false
self.parentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
}
}
https://i.stack.imgur.com/kdVQ4.png
没有一个解决方案对我有用,因为我需要动态宽度来适应 iPhone 的宽度。
class CustomLayoutFlow: UICollectionViewFlowLayout {
override init() {
super.init()
minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
minimumInteritemSpacing = 1 ; minimumLineSpacing = 1 ; scrollDirection = .horizontal
}
override var itemSize: CGSize {
set { }
get {
let width = (self.collectionView?.frame.width)!
let height = (self.collectionView?.frame.height)!
return CGSize(width: width, height: height)
}
}
}
class TextCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var textView: UITextView!
override func prepareForReuse() {
super.prepareForReuse()
}
}
class IntroViewController: UIViewController, UITextViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UINavigationControllerDelegate {
@IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
@IBOutlet weak var collectionViewTopDistanceConstraint: NSLayoutConstraint!
@IBOutlet weak var collectionView: UICollectionView!
var collectionViewLayout: CustomLayoutFlow!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionViewLayout = CustomLayoutFlow()
self.collectionView.collectionViewLayout = self.collectionViewLayout
}
override func viewWillLayoutSubviews() {
self.collectionViewTopDistanceConstraint.constant = UIScreen.main.bounds.height > 736 ? 94 : 70
self.view.layoutIfNeeded()
}
}
从 iOS 10 开始,我们有了关于流布局的新 API 来做到这一点。
您所要做的就是将您的 flowLayout.estimatedItemSize
设置为一个新常量 UICollectionViewFlowLayoutAutomaticSize
。
我遵循this answer,简单并达到目标。此外,与 selected answer 不同,它还让我可以调整 iPad 的视图。
但是我希望单元格的宽度根据设备宽度而变化,所以我没有添加宽度约束,并将 preferredLayoutAttributesFitting
方法调整为如下所示:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let targetSize = CGSize(width: UIScreen.main.bounds.size.width / ((UIDevice.current.userInterfaceIdiom == .phone) ? 1 : 2), height: 0);
layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel);
return layoutAttributes;
}
我的解决方案来自 https://www.advancedswift.com/autosizing-full-width-cells/
在您的自定义单元类中添加以下内容:
class MyCustomCell: UICollectionViewCell {
...
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
var targetSize = targetSize
targetSize.height = CGFloat.greatestFiniteMagnitude
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return size
}
}
以及包含您的集合视图的 ViewController 的 ViewDidLoad 中的以下内容:
override func viewDidLoad() {
super.viewDidLoad()
...
let cellWidth = 200 // whatever your cell width is
let layout = myCustomCollectionView.collectionViewLayout
if let flowLayout = layout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(
width: cellWidth,
height: 200 //an estimated height, but this will change when the cell is created
)
}
}
这是我找到的最简单/最短的解决方案。
systemLayoutSizeFitting
所做的唯一事情就是猜测您可能需要知道的尺寸(我认为 iOS 的其他单元格绘图部分甚至不会使用它)。请注意,它在苹果文档中清楚地说明了该调用“此方法实际上不会更改视图的大小”。
您必须在您的 collectionViewController 上继承 UICollectionViewDelegateFlowLayout 类。然后添加函数:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 100)
}
使用它,您可以获得屏幕宽度的宽度大小。
现在你有一个collectionViewController,其中行作为tableViewController。
如果您希望每个单元格的高度大小是动态的,也许您应该为您需要的每个单元格创建自定义单元格。
不定期副业成功案例分享