UICollectionView - 动态单元格高度?

UICollectionView - dynamic cell height?

我需要显示一堆具有不同高度的 collectionViewCells。视图太复杂,我不想手动计算预期高度。我想强制执行自动布局来计算单元格高度

cellForItemAtIndexPath 之外调用 dequeueReusableCellWithReuseIdentifier 会破坏 collectionView 并导致其崩溃

另一个问题是cell不在单独的xib中,所以我无法手动实例化一个临时的并用它来计算高度。

有什么解决办法吗?

public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
    
    var cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as UICollectionViewCell
    configureCell(cell, item: items[indexPath.row])
    
    cell.contentView.setNeedsLayout()
    cell.contentView.layoutIfNeeded()
    
    return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
}

编辑:

一旦调用 dequeueReusableCellWithReuseIdentifier 就会发生崩溃。如果我不调用该方法而是 return 一个大小,一切都很好,单元格显示时没有计算大小

流式布局不支持负尺寸或零尺寸

2015-01-26 18:24:34.231 [13383:9752256] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
*** First throw call stack:
(
    0   CoreFoundation                      0x00000001095aef35 __exceptionPreprocess + 165
    1   libobjc.A.dylib                     0x0000000109243bb7 objc_exception_throw + 45
    2   CoreFoundation                      0x0000000109499f33 -[__NSArrayM objectAtIndex:] + 227
    3   UIKit                               0x0000000107419d9c -[UICollectionViewFlowLayout _getSizingInfos] + 842
    4   UIKit                               0x000000010741aca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526
    5   UIKit                               0x000000010741651f -[UICollectionViewFlowLayout prepareLayout] + 257
    6   UIKit                               0x000000010742da10 -[UICollectionViewData _prepareToLoadData] + 67
    7   UIKit                               0x00000001074301c6 -[UICollectionViewData layoutAttributesForItemAtIndexPath:] + 44
    8   UIKit                               0x00000001073fddb1 -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:viewCategory:] + 248
    9                                       0x00000001042b824c _TFC1228BasePaginatingViewController14collectionViewfS0_FTCSo16UICollectionView6layoutCSo22UICollectionViewLayout22sizeForItemAtIndexPathCSo11NSIndexPath_VSC6CGSize + 700
    10                                     0x00000001042b83d4 _TToFC1228BasePaginatingViewController14collectionViewfS0_FTCSo16UICollectionView6layoutCSo22UICollectionViewLayout22sizeForItemAtIndexPathCSo11NSIndexPath_VSC6CGSize + 100
    11  UIKit                               0x0000000107419e2e -[UICollectionViewFlowLayout _getSizingInfos] + 988
    12  UIKit                               0x000000010741aca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526
    13  UIKit                               0x000000010741651f -[UICollectionViewFlowLayout prepareLayout] + 257
    14  UIKit                               0x000000010742da10 -[UICollectionViewData _prepareToLoadData] + 67
    15  UIKit                               0x000000010742e0e9 -[UICollectionViewData validateLayoutInRect:] + 54
    16  UIKit                               0x00000001073f67b8 -[UICollectionView layoutSubviews] + 170
    17  UIKit                               0x0000000106e3c973 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 521
    18  QuartzCore                          0x0000000106b0fde8 -[CALayer layoutSublayers] + 150
    19  QuartzCore                          0x0000000106b04a0e _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 380
    20  QuartzCore                          0x0000000106b0487e _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24
    21  QuartzCore                          0x0000000106a7263e _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 242
    22  QuartzCore                          0x0000000106a7374a _ZN2CA11Transaction6commitEv + 390
    23  QuartzCore                          0x0000000106a73db5 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 89
    24  CoreFoundation                      0x00000001094e3dc7 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
    25  CoreFoundation                      0x00000001094e3d20 __CFRunLoopDoObservers + 368
    26  CoreFoundation                      0x00000001094d9b53 __CFRunLoopRun + 1123
    27  CoreFoundation                      0x00000001094d9486 CFRunLoopRunSpecific + 470
    28  GraphicsServices                    0x000000010be869f0 GSEventRunModal + 161
    29  UIKit                               0x0000000106dc3420 UIApplicationMain + 1282
    30                                      0x000000010435c709 main + 169
    31  libdyld.dylib                       0x000000010a0f2145 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

这里有一个 Ray Wenderlich tutorial 向您展示如何使用 AutoLayout 来动态调整 UITableViewCell 的大小。我认为 UICollectionViewCell 也是一样的。

不过,基本上,您最终会出队并配置原型单元格并获取其高度。读完这篇文章后,我决定实现这个方法,只写一些清晰、明确的大小调整代码。

以下是我认为整篇文章的 "secret sauce":

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [self heightForBasicCellAtIndexPath:indexPath];
}

- (CGFloat)heightForBasicCellAtIndexPath:(NSIndexPath *)indexPath {
    static RWBasicCell *sizingCell = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWBasicCellIdentifier];
    });

    [self configureBasicCell:sizingCell atIndexPath:indexPath];
    return [self calculateHeightForConfiguredSizingCell:sizingCell];
}

- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    return size.height + 1.0f; // Add 1.0f for the cell separator height
}


编辑: 我对你的崩溃做了一些研究,并决定没有自定义 XIB 就无法完成这项工作。虽然这有点令人沮丧,但您应该能够从 Storyboard 剪切并粘贴到自定义的空 XIB。

完成后,如下所示的代码将助您一臂之力:

//  ViewController.m
#import "ViewController.h"
#import "CollectionViewCell.h"
@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout> {

}
@property (weak, nonatomic) IBOutlet CollectionViewCell *cell;
@property (weak, nonatomic) IBOutlet UICollectionView   *collectionView;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor lightGrayColor];
    [self.collectionView registerNib:[UINib nibWithNibName:@"CollectionViewCell" bundle:nil] forCellWithReuseIdentifier:@"cell"];
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear...");
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return 50;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
    return 10.0f;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
    return 10.0f;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return [self sizingForRowAtIndexPath:indexPath];
}
- (CGSize)sizingForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *title                  = @"This is a long title that will cause some wrapping to occur. This is a long title that will cause some wrapping to occur.";
    static NSString *subtitle               = @"This is a long subtitle that will cause some wrapping to occur. This is a long subtitle that will cause some wrapping to occur.";
    static NSString *buttonTitle            = @"This is a really long button title that will cause some wrapping to occur.";
    static CollectionViewCell *sizingCell   = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sizingCell                          = [[NSBundle mainBundle] loadNibNamed:@"CollectionViewCell" owner:self options:nil][0];
    });
    [sizingCell configureWithTitle:title subtitle:[NSString stringWithFormat:@"%@: Number %d.", subtitle, (int)indexPath.row] buttonTitle:buttonTitle];
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];
    CGSize cellSize = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    NSLog(@"cellSize: %@", NSStringFromCGSize(cellSize));
    return cellSize;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *title                  = @"This is a long title that will cause some wrapping to occur. This is a long title that will cause some wrapping to occur.";
    static NSString *subtitle               = @"This is a long subtitle that will cause some wrapping to occur. This is a long subtitle that will cause some wrapping to occur.";
    static NSString *buttonTitle            = @"This is a really long button title that will cause some wrapping to occur.";
    CollectionViewCell *cell                = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath];
    [cell configureWithTitle:title subtitle:[NSString stringWithFormat:@"%@: Number %d.", subtitle, (int)indexPath.row] buttonTitle:buttonTitle];
    return cell;
}
@end

上面的代码(连同一个非常基本的 UICollectionViewCell 子类和关联的 XIB)给了我这个:

我只是 运行 在 UICollectionView 上解决了这个问题,我解决它的方式与上面的答案类似,但是是以纯 UICollectionView 的方式。

  1. 创建一个自定义 UICollectionViewCell,其中包含您要填充的内容以使其动态化。我为它创建了自己的 .xib,因为它似乎是最简单的方法。

  2. 在该 .xib 中添加约束,允许从上到下计算单元格。如果您没有考虑所有高度,则重新调整大小将不起作用。假设您在顶部有一个视图,然后在其下方有一个标签,在其下方有另一个标签。您需要将约束连接到单元格的顶部到该视图的顶部,然后将视图的底部连接到第一个标签的顶部,第一个标签的底部到第二个标签的顶部,以及第二个标签的底部到单元格底部。

  3. 将 .xib 加载到 viewcontroller 并将其注册到 viewDidLoad

    上的 collectionView
    let nib = UINib(nibName: CustomCellName, bundle: nil)
    self.collectionView!.registerNib(nib, forCellWithReuseIdentifier: "customCellID")`
    
  4. 将该 xib 的第二个副本加载到 class 并将其存储为 属性 以便您可以使用它来确定该单元格的大小

    let sizingNibNew = NSBundle.mainBundle().loadNibNamed(CustomCellName, owner: CustomCellName.self, options: nil) as NSArray
    self.sizingNibNew = (sizingNibNew.objectAtIndex(0) as? CustomViewCell)!
    
  5. 在您的视图控制器中实现 UICollectionViewFlowLayoutDelegate。重要的方法称为 sizeForItemAtIndexPath。在该方法中,您需要从与 indexPath 中的该单元格关联的数据源中提取数据。然后配置sizingCell并调用preferredLayoutSizeFittingSize。方法 returns 一个 CGSize,它将由宽度减去内容插入和从 self.sizingCell.preferredLayoutSizeFittingSize(targetSize).

    返回的高度组成
    override func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        guard let data = datasourceArray?[indexPath.item] else {
            return CGSizeZero
        }
        let sectionInset = self.collectionView?.collectionViewLayout.sectionInset
        let widthToSubtract = sectionInset!.left + sectionInset!.right
    
        let requiredWidth = collectionView.bounds.size.width
    
    
        let targetSize = CGSize(width: requiredWidth, height: 0)
    
        sizingNibNew.configureCell(data as! CustomCellData, delegate: self)
        let adequateSize = self.sizingNibNew.preferredLayoutSizeFittingSize(targetSize)
        return CGSize(width: (self.collectionView?.bounds.width)! - widthToSubtract, height: adequateSize.height)
     }
    
  6. 在自定义单元格本身的 class 中,您需要覆盖 awakeFromNib 并告诉 contentView 它的大小需要灵活

     override func awakeFromNib() {
        super.awakeFromNib()
        self.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleHeight]
     }
    
  7. 在自定义单元格中覆盖 layoutSubviews

     override func layoutSubviews() {
       self.layoutIfNeeded()
      }
    
  8. 在自定义单元格的class中实现preferredLayoutSizeFittingSize。这是您需要对正在布置的项目进行任何欺骗的地方。如果它是一个标签,你需要告诉它它的 preferredMaxWidth 应该是多少。

    func preferredLayoutSizeFittingSize(_ targetSize: CGSize)-> CGSize {
    
        let originalFrame = self.frame
        let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth
    
    
        var frame = self.frame
        frame.size = targetSize
        self.frame = frame
    
        self.setNeedsLayout()
        self.layoutIfNeeded()
        self.label.preferredMaxLayoutWidth = self.questionLabel.bounds.size.width
    
    
        // calling this tells the cell to figure out a size for it based on the current items set
        let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
    
        let newSize = CGSize(width:targetSize.width, height:computedSize.height)
    
        self.frame = originalFrame
        self.questionLabel.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth
    
        return newSize
    }
    

所有这些步骤都应该为您提供正确的尺寸。如果您得到的是 0 或其他奇怪的数字,则表示您没有正确设置约束条件。

我们可以在没有 xib 的情况下为集合视图单元格保持动态高度(仅使用故事板)。

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout*)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath {

        NSAttributedString* labelString = [[NSAttributedString alloc] initWithString:@"Your long string goes here" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:17.0]}];
        CGRect cellRect = [labelString boundingRectWithSize:CGSizeMake(cellWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin context:nil];

        return CGSizeMake(cellWidth, cellRect.size.height);
}

确保 IB 中的 numberOfLines 应为 0。

我按照本 SO 中提到的步骤进行操作,一切正常,除非我的 Collection View 的数据(文本)较少以使其足够宽。查看 systemLyaoutSizeFittingSize 中的文档,我有这个解决方案,因此我的单元格占用了我要求的宽度:

- (CGSize)calculateSizeForSizingCell:(UICollectionViewCell *)sizingCell width:(CGFloat)width {
    CGRect frame = sizingCell.frame;
    frame.size.width = width;
    sizingCell.frame = frame;
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    CGSize size = [sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize
                        withHorizontalFittingPriority:UILayoutPriorityRequired
                              verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return size;
}

希望这对某人有所帮助。

- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);

Apple 文档:

Equivalent to sending -systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: with UILayoutPriorityFittingSizeLevel for both priorities.

根据 Apple 的文档,默认值为 "pretty low":

When you send -[UIView systemLayoutSizeFittingSize:], the size fitting most closely to the target size (the argument) is computed. UILayoutPriorityFittingSizeLevel is the priority level with which the view wants to conform to the target size in that computation. It's quite low. It is generally not appropriate to make a constraint at exactly this priority. You want to be higher or lower.

所以我对默认行为的更改是使用 UILayoutPriorityRequired 强制宽度(水平拟合)。

按照 bolnad 到第 4 步。

然后将所有其他步骤替换为:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    // Configure your cell
    sizingNibNew.configureCell(data as! CustomCellData, delegate: self)

    // We use the full width minus insets
    let width = collectionView.frame.size.width - collectionView.sectionInset.left - collectionView.sectionInset.right

    // Constrain our cell to this width 
    let height = sizingNibNew.systemLayoutSizeFitting(CGSize(width: width, height: .infinity), withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityFittingSizeLevel).height

    return CGSize(width: width, height: height)
}

TL;DR:向下扫描图像,然后查看 working project here.

正在更新我找到的更简单解决方案的答案..

就我而言,我想固定宽度,并具有可变高度的单元格。我想要一个可处理旋转且不需要太多干预的可重复使用的解决方案。

我得到的结果是在集合单元格中覆盖(仅)systemLayoutFitting(...)(在本例中是我的基础 class),并且首先击败了 UICollectionView 设置错误维度的努力contentView 通过为已知尺寸添加约束,在本例中为宽度。

class EstimatedWidthCell: UICollectionViewCell {
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.translatesAutoresizingMaskIntoConstraints = false
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        contentView.translatesAutoresizingMaskIntoConstraints = false
    }

    override func systemLayoutSizeFitting(
        _ targetSize: CGSize, withHorizontalFittingPriority
        horizontalFittingPriority: UILayoutPriority,
        verticalFittingPriority: UILayoutPriority) -> CGSize {

        width.constant = targetSize.width

然后是 return 单元格的最终大小 - 用于(这感觉像个错误)单元格本身的尺寸,而不是 contentView - 否则会被限制为大小冲突(因此存在上述约束)。为了计算正确的单元格大小,我对要浮动的维度使用了较低的优先级,然后我取回了使内容适合我要固定的宽度所需的高度:

        let size = contentView.systemLayoutSizeFitting(
            CGSize(width: targetSize.width, height: 1),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: verticalFittingPriority)

        print("\(#function) \(#line) \(targetSize) -> \(size)")
        return size
    }

    lazy var width: NSLayoutConstraint = {
        return contentView.widthAnchor
            .constraint(equalToConstant: bounds.size.width)
            .isActive(true)
    }()
}

但是这个宽度是从哪里来的呢?它是通过集合视图的流布局上的 estimatedItemSize 配置的:

lazy var collectionView: UICollectionView = {
    let view = UICollectionView(frame: CGRect(), collectionViewLayout: layout)
    view.backgroundColor = .cyan
    view.translatesAutoresizingMaskIntoConstraints = false
    return view
}()

lazy var layout: UICollectionViewFlowLayout = {
    let layout = UICollectionViewFlowLayout()
    let width = view.bounds.size.width // should adjust for inset
    layout.estimatedItemSize = CGSize(width: width, height: 10)
    layout.scrollDirection = .vertical
    return layout
}()

最后,为了处理旋转,我实施 trailCollectionDidChange 使布局无效:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    layout.estimatedItemSize = CGSize(width: view.bounds.size.width, height: 10)
    layout.invalidateLayout()
    super.traitCollectionDidChange(previousTraitCollection)
}

最终结果如下所示:

而且我已经发表了a working sample here.

Swift 4.*

我已经为 UICollectionViewCell 创建了一个 Xib,这似乎是个好方法。

extension ViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return size(indexPath: indexPath)
    }

    private func size(for indexPath: IndexPath) -> CGSize {
        // load cell from Xib
        let cell = Bundle.main.loadNibNamed("ACollectionViewCell", owner: self, options: nil)?.first as! ACollectionViewCell

        // configure cell with data in it
        let data = self.data[indexPath.item]
        cell.configure(withData: data)

        cell.setNeedsLayout()
        cell.layoutIfNeeded()

        // width that you want
        let width = collectionView.frame.width
        let height: CGFloat = 0

        let targetSize = CGSize(width: width, height: height)

        // get size with width that you want and automatic height
        let size = cell.contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .fittingSizeLevel)
        // if you want height and width both to be dynamic use below
        // let size = cell.contentView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)

        return size
    }
}

#note:在这种大小确定的情况下,我不建议在配置数据时设置图像。它给了我 distorted/unwanted 结果。配置文本只给我以下结果。

它对我有用,希望你也是。

*注意:我在Nib中使用了自动布局,记得在contentView中为子视图添加顶部和底部约束

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
           let cell = YourCollectionViewCell.instantiateFromNib()
           cell.frame.size.width = collectionView.frame.width
           cell.data = viewModel.data[indexPath.item]
           let resizing = cell.systemLayoutSizeFitting(UILayoutFittingCompressedSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.fittingSizeLevel)
           return resizing
    }

看来是个很热门的问题,所以我会尽力做出自己的微薄贡献。


下面的代码是 Swift 4 无故事板设置的解决方案。它利用了以前答案中的一些方法,因此可以防止设备旋转时引起的自动布局警告。

如果代码示例有点长,我很抱歉。我想提供完全由 Whosebug 托管的 "easy-to-use" 解决方案。如果您对 post 有任何建议 - 请分享想法,我会相应地进行更新。

设置:

两个 类:ViewController.swiftMultilineLabelCell.swift - 包含单个 UILabel 的单元格。

MultilineLabelCell.swift

import UIKit

class MultilineLabelCell: UICollectionViewCell {
    static let reuseId = "MultilineLabelCellReuseId"

    private let label: UILabel = UILabel(frame: .zero)

    override init(frame: CGRect) {
        super.init(frame: frame)

        layer.borderColor = UIColor.red.cgColor
        layer.borderWidth = 1.0

        label.numberOfLines = 0
        label.lineBreakMode = .byWordWrapping

        let labelInset = UIEdgeInsets(top: 10, left: 10, bottom: -10, right: -10)
        contentView.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor, constant: labelInset.top).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor, constant: labelInset.left).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor, constant: labelInset.right).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor, constant: labelInset.bottom).isActive = true

        label.layer.borderColor = UIColor.black.cgColor
        label.layer.borderWidth = 1.0
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("Storyboards are quicker, easier, more seductive. Not stronger then Code.")
    }

    func configure(text: String?) {
        label.text = text
    }

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.left
        layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        return layoutAttributes
    }
}

ViewController.swift

import UIKit

let samuelQuotes = [
    "Samuel says", 
    "Add different length strings here for better testing"
]

class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    private(set) var collectionView: UICollectionView

    // Initializers
    init() {
        // Create new `UICollectionView` and set `UICollectionViewFlowLayout` as its layout
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        // Create new `UICollectionView` and set `UICollectionViewFlowLayout` as its layout
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        super.init(coder: aDecoder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Dynamic size sample"

        // Register Cells
        collectionView.register(MultilineLabelCell.self, forCellWithReuseIdentifier: MultilineLabelCell.reuseId)

        // Add `coolectionView` to display hierarchy and setup its appearance
        view.addSubview(collectionView)
        collectionView.backgroundColor = .white
        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.contentInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

        // Setup Autolayout constraints
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
        collectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
        collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
        collectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true

        // Setup `dataSource` and `delegate`
        collectionView.dataSource = self
        collectionView.delegate = self

        (collectionView.collectionViewLayout as! UICollectionViewFlowLayout).estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        (collectionView.collectionViewLayout as! UICollectionViewFlowLayout).sectionInsetReference = .fromLayoutMargins
    }

    // MARK: - UICollectionViewDataSource -
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MultilineLabelCell.reuseId, for: indexPath) as! MultilineLabelCell
        cell.configure(text: samuelQuotes[indexPath.row])
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return samuelQuotes.count
    }

    // MARK: - UICollectionViewDelegateFlowLayout -
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let sectionInset = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset
        let referenceHeight: CGFloat = 100 // Approximate height of your cell
        let referenceWidth = collectionView.safeAreaLayoutGuide.layoutFrame.width
            - sectionInset.left
            - sectionInset.right
            - collectionView.contentInset.left
            - collectionView.contentInset.right
        return CGSize(width: referenceWidth, height: referenceHeight)
    }
}

要运行这个示例创建新的Xcode项目,创建相应的文件并用以下代码替换AppDelegate内容:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var navigationController: UINavigationController?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)

        if let window = window {
            let vc = ViewController()
            navigationController = UINavigationController(rootViewController: vc)
            window.rootViewController = navigationController
            window.makeKeyAndVisible()
        }

        return true
    }
}

Swift 4 个答案基于@mbm29414 的有用回答。

不幸的是,它需要使用 XIB 文件。似乎没有其他选择。

关键部分是使用调整大小的单元格(只创建一次)并在初始化集合视图时注册 XIB。

然后在 sizeForItemAt 函数中动态调整每个单元格的大小。

// UICollectionView Vars and Constants
let CellXIBName = YouViewCell.XIBName
let CellReuseID = YouViewCell.ReuseID
var sizingCell = YouViewCell()


fileprivate func initCollectionView() {
    // Connect to view controller
    collectionView.dataSource = self
    collectionView.delegate = self

    // Register XIB
    collectionView.register(UINib(nibName: CellXIBName, bundle: nil), forCellWithReuseIdentifier: CellReuseID)

    // Create sizing cell for dynamically sizing cells
    sizingCell = Bundle.main.loadNibNamed(CellXIBName, owner: self, options: nil)?.first as! YourViewCell

    // Set scroll direction
    let layout = UICollectionViewFlowLayout()
    layout.scrollDirection = .vertical
    collectionView.collectionViewLayout = layout

    // Set properties
    collectionView.alwaysBounceVertical = true
    collectionView.alwaysBounceHorizontal = false

    // Set top/bottom padding
    collectionView.contentInset = UIEdgeInsets(top: collectionViewTopPadding, left: collectionViewSidePadding, bottom: collectionViewBottomPadding, right: collectionViewSidePadding)

    // Hide scrollers
    collectionView.showsVerticalScrollIndicator = false
    collectionView.showsHorizontalScrollIndicator = false
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    // Get cell data and render post
    let data = YourData[indexPath.row]
    sizingCell.renderCell(data: data)

    // Get cell size
    sizingCell.setNeedsLayout()
    sizingCell.layoutIfNeeded()
    let cellSize = sizingCell.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)

    // Return cell size
    return cellSize
}