UICollectionView,全宽单元格,允许自动布局动态高度?

注意 For 2021! 请参见@Ely 关于 UICollectionLayoutListConfiguration的回答! ! !


在一个垂直的 UICollectionView里,

是否可能有 全宽度单元格全宽度单元格,但是,允许 动态高度自动输出控制?

这也许是“ iOS 系统中最重要的问题,却没有一个真正好的答案。”


重要提示:

请注意,在99% 的情况下,实现全宽单元格 + 自动布局动态高度,简单的 使用表格视图。就是这么简单。


那么需要集合视图的示例是什么呢?

集合视图比表视图强大得多。

一个简单的例子是,必须使用具有自动布局动态高度的集合视图:

如果在集合视图的两个布局之间使用 充满活力。例如,当设备旋转时,在1和2列布局之间。

这是 iOS 中的一个常用语。不幸的是,这只能通过解决质量保证中提出的问题来实现。:-/

130363 次浏览

您必须继承 CollectionViewController 上的类 UICollectionViewgenerateFlowLayout,然后添加函数:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 100)
}

使用它,您可以得到屏幕宽度的宽度大小。

现在您有了一个 CollectionViewController,其中行作为 tableViewController。

如果希望每个单元格的高度大小是动态的,也许应该为所需的每个单元格创建自定义单元格。

有几种方法可以解决这个问题。

一种方法是您可以给集合视图流布局一个估计的大小,并使用 systemLayoutSizeFitting来计算单元格大小。

注意: 正如在下面的评论中提到的,从 iOS 10开始,你不再需要提供一个估计的大小来触发对手机上 func preferredLayoutAttributesFitting(_ layoutAttributes:) < em > 的调用。以前(iOS9) ,如果您想要调用 priredLayoutAttritribute,则需要提供一个估计的大小。

(假设您正在使用故事板,并且集合视图是通过 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)
}

虽然这些示例不是完美的生产准备示例,但它们应该可以让您朝着正确的方向开始。我不能说这是最佳实践,但是这对我来说是有效的,即使在包含多个标签的相当复杂的单元格中,这些单元格可能包装成多行,也可能不包装成多行。

不知道这算不算是一个“非常好的答案”,但我正是用它来完成这个任务的。我的流布局是水平的,我试图使宽度调整与自动布局,所以它类似于您的情况。

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。在您的情况下,它应该能够根据布局中可用的新约束进行更新。

就我个人而言,我发现使用 UICollectionView 的最佳方法是使用一个实际的单元格来测量大小,同时使用 AutoLayout 确定大小,而每个单元格可以有不同的大小。

我在 我的博客文章的一集里说过这个

希望这个能帮助你达到你的目的。我不是100% 肯定,但我相信不像 UITableView,你实际上可以有一个完全自动的单元格高度使用自动布局结合

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView 没有这种让 AutoLayout 决定大小的方法,因为 UICollectionViewCell 不一定会填满整个屏幕的宽度。

但是这里有一个问题要问您 : 如果您需要全屏宽度的单元格,那么为什么还要费心使用 UICollectionView 而不是自动调整单元格大小的老式 UITableView 呢?

从 iOS10开始,我们有了新的 API 来实现这一点。

你所要做的就是将你的 flowLayout.estimatedItemSize设置为一个新的常量,UICollectionViewFlowLayoutAutomaticSize

来源

问题

您正在寻找自动高度,也希望有全宽, 不可能同时使用 UICollectionViewFlowLayoutAutomaticSize

你想做的使用 UICollectionView,所以下面是为你的解决方案。

解决方案

步骤-I : 计算单元格的预期高度

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,并且假设它在 Height 中是动态的,那么您需要获得 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的大小

在 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)
}

我希望这能帮到你。

viewDidLayoutSubviews上,将 estimatedItemSize设置为全宽(布局指的是 UICollectionViewFlowLayout 对象) :

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.bounds.size.width, height: 120)
}

在单元格中,确保约束同时触及单元格的顶部和底部(下面的代码使用 Cartography 来简化约束的设置,但如果需要,也可以使用 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
}

瞧,你们的细胞现在可以在高度上自动生长了!

所有的解决方案都不适合我,因为我需要动态宽度来适应不同的 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()
}
}

根据我对 Eric 答案的评论,我的解决方案与他的非常相似,但是我必须在 preredSizeFor... 中添加一个约束,以便约束到固定维度。

    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
}

这个问题有很多重复的 我在这里详细地回答了, 并在这里提供了一个工作样本应用程序。

对于这个问题,我找到了一个相当简单的解决方案: 在 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

结果是一个具有动态高度的全宽单元格。

enter image description here

必须向 CollectionViewCell 添加宽度约束

class SelfSizingCell: UICollectionViewCell {


override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
}
}
  1. 设置流程布局的 estimatedItemSize:

    collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
    
  2. Define a width constraint in the cell and set it to be equal to superview's width:

    class CollectionViewCell: UICollectionViewCell {
    private var widthConstraint: NSLayoutConstraint?
    
    
    ...
    
    
    override init(frame: CGRect) {
    ...
    // Create width constraint to set it later.
    widthConstraint = contentView.widthAnchor.constraint(equalToConstant: 0)
    }
    
    
    override func updateConstraints() {
    // Set width constraint to superview's width.
    widthConstraint?.constant = superview?.bounds.width ?? 0
    widthConstraint?.isActive = true
    super.updateConstraints()
    }
    
    
    ...
    }
    

Full example

Tested on iOS 11.

1. iOS 13 + 解决方案

有了 Swift 5.1和 iOS 13,你可以使用 组合布局对象来解决你的问题。

下面完整的示例代码显示了如何在全宽 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")
}


}

预期展品:

enter image description here


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
}

预期展品:

enter image description here

在 IOS 上测试: 12.1 Swift 4.1

我有一个非常简单的解决方案,不需要打破约束。

enter image description here

我的 ViewControllerClass

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
}




}

CustomCell 类别:

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 的 ABc0功能。我们还必须在 Cell 中设置视图的宽度。

AutoLayout 可以用于 CollectionView 中单元格的自动大小调整,只需简单的两步:

  1. 启用动态单元大小调整

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. 拥有一个容器视图并设置来自 collectionView(:cellForItemAt:)的 conterView.widthAnchor.約束,以将 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
}
}


....
}

就是这样,你会得到你想要的结果。 完整代码请参考以下要点:

参考/学分:

  • V8tr 的博客文章-< a href = “ https://www.vadibulavin.com/Collection-View-Cells-self-size/”rel = “ nofollow noReferrer”> Collection View Cell Self-Size: Step by Step 教程
  • 另一个堆栈溢出的答案

截图: enter image description here

我还面临着动态单元的高度问题,可以解决这个问题使用自动布局和手动高度计算。不需要使用估计的大小,实例化或创建单元格。

此外,这个解决方案还提供了对多行标签的处理。它基于计算单元格中所有子视图的子视图高度。

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)
}
}

UICollectionViewgenerateFlowLayout 使用单元格的大小,这是用 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

我遵循 这个答案,简单,达到了目标。另外,它还能让我在 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;
}

如果您使用的是 IOS14或更新的版本,那么您可以使用 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: 实践现代收藏观的文章中找到,特别是以 创建简单列表布局开头的部分。

示例项目包含一个名为 控制器的控制器,该控制器演示如何基于自动布局配置自动高度单元格。

我的解决方案是从 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
)
}
}

这是我找到的最简单/最短的解决方案。

我已经看到了许多动态高度细胞的复杂答案。然而,经过几次谷歌搜索,我找到了这个简单的答案。

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
}

}

参考截图。 enter image description here

使用 UICollectionViewCompositionalLayout时,请确保将 itemSizegroupSize的高度都设置为 .estimated:

我的问题是,我使用 .fractional(1)的项目 height.estimated(44)group,它不工作。

extension TasksController {
static func createLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { (sectionIndex: Int, _ NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let height: CGFloat = 44


let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(height))
let item = NSCollectionLayoutItem(layoutSize: itemSize)




let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(height))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns)


let section = NSCollectionLayoutSection(group: group)
return section
}
}
}