Подтвердить что ты не робот

UICollectionView - динамическая высота ячейки?

Мне нужно отобразить кучу collectionViewCells с разной высотой. представления слишком сложны, и я не хочу вручную вычислять ожидаемую высоту. Я хочу обеспечить автоматическую компоновку для расчета высоты ячейки

Вызов dequeueReusableCellWithReuseIdentifier вне cellForItemAtIndexPath прерывает collectionView и вызывает его сбой

Другая проблема заключается в том, что ячейка не находится в отдельном 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)
}

EDIT:

Сбой происходит, как только вызывается dequeueReusableCellWithReuseIdentifier. Если я не назову этот метод и вместо этого верну его размер, все будет отлично работать, а ячейки будут отображаться без расчетного размера

отрицательные или нулевые размеры не поддерживаются в макете потока

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
4b9b3361

Ответ 1

Вот учебник Ray Wenderlich, в котором показано, как использовать AutoLayout для динамического размера UITableViewCell s. Я бы подумал, что это будет одинаково для UICollectionViewCell.

В принципе, вы заканчиваете удаление и настройку прототипа ячейки и захват ее высоты. Прочитав эту статью, я решил НЕ реализовать этот метод и просто написать ясный явный код размера.

Вот что я считаю "секретным соусом" для всей статьи:

- (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. Хотя это немного разочаровывает, вы должны иметь возможность вырезать и вставлять с вашей раскадровки в пользовательский пустой 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) дает мне следующее:

enter image description here

Ответ 2

Я просто столкнулся с этой проблемой в UICollectionView и тем, как я решил ее, аналогично приведенному выше, но чистым способом UICollectionView.

  • Создайте пользовательский UICollectionViewCell, который содержит все, что вы будете заполнять, чтобы сделать его динамичным. Я создал для себя свой собственный .xib, поскольку он выглядит как самый простой подход.

  • Добавьте ограничения в этот .xib, которые позволяют вычислять ячейку сверху вниз. Повторная калибровка не будет работать, если вы не указали всю высоту. Скажем, у вас есть вид сверху, затем надпись под ним и еще одна надпись. Вам нужно будет подключить ограничения к вершине ячейки вверху этого представления, затем к нижней части окна вверху первой метки, нижней части первой метки к верхней части второй метки и нижней части второй метки к нижней части ячейки.

  • Загрузите .xib в viewcontroller и зарегистрируйте его с помощью коллекцииView на viewDidLoad

    let nib = UINib(nibName: CustomCellName, bundle: nil)
    self.collectionView!.registerNib(nib, forCellWithReuseIdentifier: "customCellID")`
    
  • Загрузите вторую копию этого xib в класс и сохраните его как свойство, чтобы вы могли использовать его для определения размера того, что должна быть в этой ячейке

    let sizingNibNew = NSBundle.mainBundle().loadNibNamed(CustomCellName, owner: CustomCellName.self, options: nil) as NSArray
    self.sizingNibNew = (sizingNibNew.objectAtIndex(0) as? CustomViewCell)!
    
  • Внедрите UICollectionViewFlowLayoutDelegate в свой контроллер. Метод, который имеет значение, называется sizeForItemAtIndexPath. Внутри этого метода вам нужно будет вытащить данные из источника данных, который связан с этой ячейкой из indexPath. Затем настройте sizingCell и вызовите preferredLayoutSizeFittingSize. Метод возвращает 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)
     }
    
  • В классе собственной ячейки вам нужно переопределить awakeFromNib и сообщить contentView, что его размер должен быть гибким

     override func awakeFromNib() {
        super.awakeFromNib()
        self.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleHeight]
     }
    
  • В пользовательском переопределении ячеек layoutSubviews

     override func layoutSubviews() {
       self.layoutIfNeeded()
      }
    
  • В классе пользовательского набора ячеек preferredLayoutSizeFittingSize. Здесь вам понадобятся какие-либо обманки на предметах, которые выложены. Если его ярлык вам нужно будет указать, какой должна быть его предпочтительнаяMaxWidth.

    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 или другие напуганные цифры, чем вы неправильно настроили свои ограничения.

Ответ 3

Мы можем поддерживать динамическую высоту для ячейки просмотра коллекции без 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);
}

Убедитесь, что numberOfLines в IB должно быть 0.

Ответ 4

TL; DR: Сканирование до изображения, а затем проверить рабочий проект здесь.

Обновление моего ответа для более простого решения, которое я нашел..

В моем случае я хотел зафиксировать ширину и иметь ячейки переменной высоты. Я хотел использовать многоразовое решение, которое бы обрабатывало вращение и не требовало большого вмешательства.

Я пришел к systemLayoutFitting(...), что переопределил (просто) systemLayoutFitting(...) в ячейке коллекции (в данном случае это базовый класс для меня), и сначала победил попытку 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

и затем возвращает окончательный размер для ячейки - используется для (и это похоже на ошибку) измерения самой ячейки, но не 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)
}

Конечный результат выглядит так:

enter image description here

И я опубликовал рабочий образец здесь.

Ответ 5

Swift 4. *

Я создал Xib для UICollectionViewCell, который кажется хорошим подходом.

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

# примечание: я не рекомендую устанавливать изображение при настройке данных в этом случае, определяющем размер. Это дало мне искаженный/нежелательный результат. Настройка текстов только дала мне результат ниже.

enter image description here

Ответ 6

Кажется, это довольно популярный вопрос, поэтому я постараюсь внести свой скромный вклад.


Код ниже - решение Swift 4 для установки без раскадровки. Он использует некоторые подходы из предыдущих ответов, поэтому он предотвращает предупреждение Auto Layout, возникающее при повороте устройства.

Прошу прощения, если примеры кода немного длинны. Я хочу предоставить "простое в использовании" решение, полностью размещенное на StackOverflow. Если у вас есть какие-либо предложения к посту - поделитесь идеей, и я обновлю ее соответственно.

Настройка:

Два класса: ViewController.swift и MultilineLabelCell.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)
    }
}

Для запуска этого примера создайте новый проект AppDelegate, создайте соответствующие файлы и замените содержимое 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
    }
}

Ответ 7

Я выполнил шаги, упомянутые в этом 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 doc:

Эквивалент отправке -системеLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority: с UILayoutPriorityFittingSizeLevel для обоих приоритетов.

Пока значение по умолчанию "довольно мало" в соответствии с документом Apple:

При отправке - [UIView systemLayoutSizeFittingSize:] вычисляется размер, наиболее подходящий к размеру цели (аргументу). UILayoutPriorityFittingSizeLevel - это уровень приоритета, с которым представление хочет соответствовать целевому размеру в этом вычислении. Это довольно низко. Как правило, нецелесообразно ограничивать этот приоритет. Вы хотите быть выше или ниже.

Таким образом, мое изменение поведения по умолчанию заключается в обеспечении ширины (горизонтальной установки) с помощью UILayoutPriorityRequired.

Ответ 8

Следуйте bolnad answer до шага 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)
}

Ответ 9

Это сработало для меня, надеюсь, вы тоже.

* Примечание: я использовал автоматическое расположение в 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
    }

Ответ 10

Ответ Swift 4 на основе полезного ответа от @mbm29414.

К сожалению, это требует использования файла XIB. Там, кажется, нет альтернативы.

Ключевые части используют ячейку определения размера (создается только один раз) и регистрируют XIB при инициализации представления сбора.

Затем вы динамически sizeForItemAt размер каждой ячейки в функции 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
}