ChatGPT解决这个技术问题 Extra ChatGPT

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

注意 2021 年!请参阅关于 UICollectionLayoutListConfiguration 的@Ely 答案!!!!

在垂直 UICollectionView 中,

是否可以有全角单元格,但是,允许动态高度由自动布局控制?

这让我觉得这可能是“iOS 中最重要的问题,但没有真正好的答案”。

重要的:

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

那么你需要一个集合视图的例子是什么?

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

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

如果您在集合视图中的两个布局之间进行动画处理。例如,在 1 和 2 列布局之间,当设备旋转时。

这是iOS中的一个常见习语。不幸的是,它只能通过解决此 QA 中提出的问题来实现。 :-/

是的,可以使用 UICollectionViewDelegateFlowLayout 类
这是否意味着您实际上可以使用 tableView?你需要什么额外的功能才能使这个复杂化?
哦,您是否尝试过使用列表视图的 Bouncy 列表? @Fattie
嗨@dhiru,这个问题是关于 UICollectionView 的。您可能会感到困惑,顺便提到了“弹性”,它与任何事情无关。问题是关于 UICollectionView。谢谢
嗨@Fattie,我很好,希望你也做得很好。我也没有找到解决办法。我试了一下,我认为我朝着一个好的方向前进,但这也不完美。另一个引入的错误有时单元格会重叠。 AutoLayout 对于复杂的 UI 来说真的很糟糕。

I
Imanou Petit

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


每当大小更改时,CollectionView 都会滚动到顶部
@Fattie 我已经更新了我对 Swift 5.1 和 iOS 13 的回答。使用 Compositional Layout 对象,现在事情比以往任何时候都容易。
@ImanouPetit,我将提出一个更有趣的问题。如果你想用“真正的” UICollectionViewLayout 来做,也就是说不使用流布局怎么办。 !我想知道?
我使用了 iOS12 解决方案(在 Xcode11/iOS13 中),并且必须在 PreferredLayoutAttributesFitting() 方法中的 layoutIfNeeded 上方添加 setNeedsLayout。如果没有这个,我的一些自动布局约束不会在调整单元格大小时更新。
我对组合布局有疑问,不得不回退到流布局。问题是我的单元格的高度范围很广,从 50 到 600。当我给出的估计太小时,单元格(也是一个集合视图)没有增长到足够大的大小。如果我给出的估计太高,那么可能会出现非常大的空白。我真的把头发扯掉了,发现没有办法克服这个问题。
F
Fattie

问题

您正在寻找自动高度并且还希望有完整的宽度,使用 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)
}

我希望这会帮助你。


是否可以有一个水平滚动的 UICollectionView 单元格高度随着自动布局垂直增加?
我想完成相同但一半的宽度和动态高度
return CGSize(width: view.frame.width/2, height: expectedHeight) 还要考虑 Padding 而不是返回宽度,否则两个单元格将不适合单行。
@nr5 你得到解决方案了吗?我的要求和你一样。谢谢。
@MaulikBhuptani 我无法使用自动布局完全做到这一点。必须创建一个自定义布局类并覆盖一些类方法。
E
Eric Murphey

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

一种方法是您可以为集合视图流布局提供估计大小并使用 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)
}

虽然这些不是完美的生产就绪示例,但它们应该让您朝着正确的方向开始。我不能说这是最佳实践,但这对我有用,即使包含多个标签的相当复杂的单元格,可能会或可能不会换行。


@Fattie 不清楚“esitmatedItemSize”如何与动态大小的集合视图单元格无关,所以我想听听您的想法。 (不是讽刺)
此外,如果此答案缺少特定的有用信息和/或您认为规范的内容,请告诉我。我不会争论赏金,我只是想对任何看到这个问题的人有所帮助。
(包括或不包括estimatedItemSize在当前iOS中没有任何区别,并且在“全宽+动态高度”问题中根本没有帮助。)最后的代码,这些天很少使用笔尖并且它有与动态高度几乎没有相关性(例如,来自一些动态高度文本视图)。不过谢谢
我也找到了这个解决方案,但在我的情况下,iOS 11、Xcode 9.2,单元格内容似乎与单元格本身断开了连接——我无法将图像扩展到固定尺寸的完整高度或宽度。我进一步注意到 CV 是 preferredLayoutAttributesFitting 中的设置约束。然后我在所述函数中添加了我自己的约束,绑定到来自估计项大小的固定维度,FWIW,请参阅下面的答案。
i
inf1783

我为这个问题找到了一个非常简单的解决方案:在我的 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


只是 TBC @inf1783,你是在 UICollectionView 做这个吗? (不是表格视图) - 正确吗?
@Fattie 是的,这是一个 UICollectionView ;)
太棒了@inf1783,迫不及待想试试这个。顺便说一句,另一个好方法是子类化 UIView 并简单地添加一个内在宽度(我猜它会产生相同的效果,并且它可能也会使其在情节提要时工作)
每次我尝试这个时,我都会陷入无限循环,UICollectionViewFlowLayout 的行为未定义错误:/
如果您设置了插入,这种方法很容易出错,它会使单元格太宽而无法容纳内容,并且通常很脆弱。
A
Abhishek Biswas

在职的!!!在 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 方法来实现。
V
Vignesh

如果您使用的是 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 的控制器,它显示了如何基于自动布局配置自动高度单元格。


M
Manuco Bianco

设置流布局的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() } ... }

Full example

在 iOS 11 上测试。


M
Mecid

您必须向 CollectionViewCell 添加宽度约束

class SelfSizingCell: UICollectionViewCell {

  override func awakeFromNib() {
      super.awakeFromNib()
      contentView.translatesAutoresizingMaskIntoConstraints = false
      contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
  }
}

这会将单元格限制为固定宽度,几乎不会自行调整大小。如果您要在 iPad 上旋转设备或调整屏幕尺寸,它将保持固定。
这将导致collectionview 不能平滑滚动。
M
Manuco Bianco

就个人而言,我发现拥有一个 UICollectionView 的最佳方法,其中 AutoLayout 确定大小,而每个 Cell 可以具有不同的大小,即在使用实际 Cell 测量大小的同时实现 UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath 函数。

我在 my blog posts 之一中谈到了这一点

希望这个能帮助你实现你想要的。我不是 100% 确定,但我相信与 UITableView 不同的是,您实际上可以通过使用 AutoLayout 结合使用来获得全自动的单元格高度

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

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

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


等一下,下面的Aprit说UICollectionViewFlowLayoutAutomaticSize现在可以在流布局中使用,在iOS10中......
我也刚看到这个。显然,iOS 10 现在就是这样。我刚刚观看了相应的 WWDC 讨论:developer.apple.com/videos/play/wwdc2016/219 但是,这将完全自动调整单元格大小以适应其内容。我不确定如何告诉单元格(使用 AutoLayout)填充屏幕宽度,因为您无法在 UICollectionViewCell 与其父级之间设置约束(至少在 StoryBoards 中没有)
根据谈话,您仍然可以覆盖 sizeThatFits() 或 preferredLayoutAttributesFitting() 并自己计算尺寸,但这不是我认为的 OP 要求的。无论如何,我仍然会对他/她需要全宽 UICollectionViewCell 的用例以及为什么在这种特殊情况下不使用 UITableView 会如此重要(当然可能存在某些情况但我猜你只需要自己处理一些计算)
想一想,你不能简单地设置“columns == 1”(然后当设备横向放置时可能是 columns == 2),使用集合视图,这是非常不可思议的。
那么你可以编写一个自定义布局来做到这一点。它实际上并没有那么复杂,而且流程布局在很多情况下都做得很好。我们不能指望苹果涵盖所有情况,这肯定会在其他地方引起一些冲突。您基本上可以通过覆盖 sizeForItemAt 以及我在博客文章中描述的大小调整单元格来实现 1/2 列布局。那真的很简单,只需要几行代码。
M
Manuco Bianco

根据我对 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.


M
Mark Suman

不确定这是否符合“非常好的答案”,但这是我用来完成此任务的。我的流布局是水平的,我正在尝试使用自动布局调整宽度,所以它与您的情况相似。

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


只是 TBC 标记,你的意思是你实际上是在改变一个单元格的大小吗? (即,单元格出现并且大小为 X,然后您的应用程序中发生了一些事情,它变成了大小 Y ..?)thx
是的。当用户在屏幕上拖动一个元素时,会触发一个更新单元格大小的事件。
R
Raphael

在您的 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
    }

瞧,你的细胞现在会自动长高!


A
Alex

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


V
Volodymyr

我还遇到了动态单元格的高度问题,可以使用自动布局和手动高度计算来解决这个问题。无需使用估计大小、实例化或创建单元格。

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

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


I
Irtaza fayaz

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

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


i
iOS Flow

没有一个解决方案对我有用,因为我需要动态宽度来适应 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()
        }
    }

M
Manuco Bianco

从 iOS 10 开始,我们有了关于流布局的新 API 来做到这一点。

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

Source


嗯,你很确定它是*全宽的?那么,本质上是一列?
如果您将 UICollectionViewCell 的宽度显式设置为全宽,则可能会出现这种情况。
不,它不会将宽度固定为全高,并且会忽略估计的尺寸值,除非您按照上面 Eric 的回答,或者在下面/以后进行我的回答。
好的,不幸的是,这与问题完全无关!呵呵! :O
M
Mostafa Al Belliehy

我遵循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;
}

i
iOS_Mouse

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

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


??????这只是将单元格宽度设置为 200。
另外,请注意这篇文章似乎有很多问题。 systemLayoutSizeFitting 所做的唯一事情就是猜测您可能需要知道的尺寸(我认为 iOS 的其他单元格绘图部分甚至不会使用它)。请注意,它在苹果文档中清楚地说明了该调用“此方法实际上不会更改视图的大小”。
F
Fattie

您必须在您的 collectionViewController 上继承 UICollectionViewDelegateFlowLayout 类。然后添加函数:

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

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

现在你有一个collectionViewController,其中行作为tableViewController。

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


这完全不是问题的答案 :) 这将给出 100 的固定高度,即,这是在自动布局存在之前编程的方式。