[ios] UICollectionView - dynamic cell height?

I need to display a bunch of collectionViewCells that have different heights. the views are too complex and I don't want to manually calculate the expected height. I want to enforce auto-layout to calculate cell height

Calling dequeueReusableCellWithReuseIdentifier outside of cellForItemAtIndexPath breaks collectionView and causes it to crash

Another problem is the cell is not in a separate xib, so I can't manually instantiate a temporary one and use it for height calculation.

Any solutions for this?

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:

Crash happens as soon as dequeueReusableCellWithReuseIdentifier is called. If I don't call that method and instead return a size everything works great and cells show up without the calculated size

negative or zero sizes are not supported in the flow layout

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

This question is related to ios swift uicollectionview

The answer is


It worked for me, hope you too.

*Note: I have used auto layout in Nib, remember add top and bottom contraints for subviews in 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
    }

Follow bolnad answer up to Step 4.

Then make it simpler by replacing all the other steps with:

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

Seems like it's quite a popular question, so I will try to make my humble contribution.


The code below is Swift 4 solution for no-storyboard setup. It utilizes some approaches from previous answers, therefore it prevents Auto Layout warning caused on device rotation.

I am sorry if code samples are a bit long. I want to provide an "easy-to-use" solution fully hosted by StackOverflow. If you have any suggestions to the post - please, share the idea and I will update it accordingly.

The setup:

Two classes: ViewController.swift and MultilineLabelCell.swift - Cell containing single 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)
    }
}

To run this sample create new Xcode project, create corresponding files and replace AppDelegate contents with the following code:

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

TL;DR: Scan down to image, and then check out working project here.

Updating my answer for a simpler solution that I found..

In my case, I wanted to fix the width, and have variable height cells. I wanted a drop in, reusable solution that handled rotation and didn't require a lot of intervention.

What I arrived at, was override (just) systemLayoutFitting(...) in the collection cell (in this case a base class for me), and first defeat UICollectionView's effort to set the wrong dimension on contentView by adding a constraint for the known dimension, in this case, the width.

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

and then return the final size for the cell - used for (and this feels like a bug) the dimension of the cell itself, but not contentView - which is otherwise constrained to a conflicting size (hence the constraint above). To calculate the correct cell size, I use a lower priority for the dimension that I wanted to float, and I get back the height required to fit the content within the width to which I want to fix:

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

But where does this width come from? It is configured via the estimatedItemSize on the collection view's flow layout:

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

Finally, to handle rotation, I implement trailCollectionDidChange to invalidate the layout:

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

The final result looks like this:

enter image description here

And I have published a working sample here.


Swift 4 answer based on helpful answer from @mbm29414.

Unfortunately, it requires the use of a XIB file. There doesn't appear to be an alternative.

The key parts are using a sizing cell (created only once) and registering the XIB when initializing the collection view.

Then you size each cell dynamically within the sizeForItemAt function.

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

I just ran into this problem on a UICollectionView and the way that i solved it similar to the answer above but in a pure UICollectionView way.

  1. Create a custom UICollectionViewCell that contains whatever you will be filling it with to make it dynamic. I created its own .xib for it as it seems like the easiest approach.

  2. Add constraints in that .xib that allow for the cell to be calculated from top to bottom. The re-sizing won't work if you haven't accounted for all of the height. Say you have a view on top, then a label underneath it, and another label underneath that. You would need to connect constraints to the top of the cell to the top of that view, then the bottom of the view to the top of the first label, bottom of first label to the top of the second label, and bottom of second label to bottom of cell.

  3. Load the .xib into the viewcontroller and register it with the collectionView on viewDidLoad

    let nib = UINib(nibName: CustomCellName, bundle: nil)
    self.collectionView!.registerNib(nib, forCellWithReuseIdentifier: "customCellID")`
    
  4. Load a second copy of that xib into the class and store it as a property so you can use it to determine the size of what that cell should be

    let sizingNibNew = NSBundle.mainBundle().loadNibNamed(CustomCellName, owner: CustomCellName.self, options: nil) as NSArray
    self.sizingNibNew = (sizingNibNew.objectAtIndex(0) as? CustomViewCell)!
    
  5. Implement the UICollectionViewFlowLayoutDelegate in your view controller. The method that matters is called sizeForItemAtIndexPath. Inside that method you will need to pull the data from the datasource that is associated with that cell from the indexPath. Then configure the sizingCell and call preferredLayoutSizeFittingSize. The method returns a CGSize which will consist of the width minus the content insets and the height that is returned from 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. In the class of the custom cell itself you will need to override awakeFromNib and tell the contentView that its size needs to be flexible

     override func awakeFromNib() {
        super.awakeFromNib()
        self.contentView.autoresizingMask = [UIViewAutoresizing.FlexibleHeight]
     }
    
  7. In the custom cell override layoutSubviews

     override func layoutSubviews() {
       self.layoutIfNeeded()
      }
    
  8. In the class of the custom cell implement preferredLayoutSizeFittingSize. This is where you will need to do any trickery on the items that are being laid out. If its a label you will need to tell it what its preferredMaxWidth should be.

    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
    }
    

All those steps should give you the correct sizes. If your getting 0 or other funky numbers than you haven't set up your constraints properly.


We can maintain dynamic height for collection view cell without xib(only using storyboard).

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

Make sure that numberOfLines in IB should be 0.


Swift 4.*

I have created a Xib for UICollectionViewCell which seems to be the good approach.

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: I don't recommend setting image when configuring data in this size determining case. It gave me the distorted/unwanted result. Configuring texts only gave me below result.

enter image description here


I followed the steps mentioned in this SO and everything is fine except when my Collection View has less data (text) to make it wide enough. Checking the documentation in systemLyaoutSizeFittingSize, I have this solution so my cell take up the width as I requested:

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

Hope this would help someone.

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

Apple doc:

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

While the default value is "pretty low" according to Apple's doc:

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.

So my change of default behavior is to enforce the width (horizontal fitting) with UILayoutPriorityRequired.


Examples related to ios

Adding a UISegmentedControl to UITableView Crop image to specified size and picture location Undefined Symbols error when integrating Apptentive iOS SDK via Cocoapods Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Autoresize View When SubViews are Added Warp \ bend effect on a UIView? Speech input for visually impaired users without the need to tap the screen make UITableViewCell selectable only while editing Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Examples related to swift

Make a VStack fill the width of the screen in SwiftUI Xcode 10.2.1 Command PhaseScriptExecution failed with a nonzero exit code Command CompileSwift failed with a nonzero exit code in Xcode 10 Convert Json string to Json object in Swift 4 iOS Swift - Get the Current Local Time and Date Timestamp Xcode 9 Swift Language Version (SWIFT_VERSION) How do I use Safe Area Layout programmatically? How can I use String substring in Swift 4? 'substring(to:)' is deprecated: Please use String slicing subscript with a 'partial range from' operator Safe Area of Xcode 9 The use of Swift 3 @objc inference in Swift 4 mode is deprecated?

Examples related to uicollectionview

How to set UICollectionViewCell Width and Height programmatically How to make a simple collection view with Swift How to set cell spacing and UICollectionView - UICollectionViewFlowLayout size ratio? UICollectionView - dynamic cell height? UICollectionView Self Sizing Cells with Auto Layout Fatal error: unexpectedly found nil while unwrapping an Optional values Paging UICollectionView by cells, not screen UICollectionView - Horizontal scroll, horizontal layout? UICollectionView current visible cell index Creating a UICollectionView programmatically