194

Note For 2021! See @Ely answer regarding UICollectionLayoutListConfiguration !!!!


In a vertical UICollectionView ,

Is it possible to have full-width cells, but, allow the dynamic height to be controlled by autolayout?

This strikes me as perhaps the "most important question in iOS with no really good answer."


Important:

Note that in 99% of cases, to achieve full width cells + autolayout dynamic height, simply use a table view. It's that easy.


So what's an example of where you need a collection view?

Collection views are far more powerful than table views.

One straightforward example where you must use a collection view with autolayout dynamic height:

If you animate between two layouts in a collection view. For example, between a 1 and 2 column layout, when the device rotates.

That's a common idiom in iOS. Unfortunately it can only be achieved by solving the problem posed in this QA. :-/

Fattie
  • 27,874
  • 70
  • 431
  • 719

21 Answers21

242

1. Solution for iOS 13+

With Swift 5.1 and iOS 13, you can use Compositional Layout objects in order to solve your problem.

The following complete sample code shows how to display multiline UILabel inside full-width UICollectionViewCell:

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

}

Expected display:

enter image description here


2. Solution for iOS 11+

With Swift 5.1 and iOS 11, you can subclass UICollectionViewFlowLayout and set its estimatedItemSize property to UICollectionViewFlowLayout.automaticSize (this tells the system that you want to deal with autoresizing UICollectionViewCells). You'll then have to override layoutAttributesForElements(in:) and layoutAttributesForItem(at:) in order to set cells width. Lastly, you'll have to override your cell's preferredLayoutAttributesFitting(_:) method and compute its height.

The following complete code shows how to display multiline UILabel inside full-width UIcollectionViewCell (constrained by UICollectionView's safe area and UICollectionViewFlowLayout's insets):

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
    }

}

Here are some alternative implementations for 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
}

Expected display:

enter image description here

Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
  • This worked well for me until I added section headers. Section headers are misplaced and cover other cells in iOS 11. But works fine in iOS 12. Did you face any issue with adding section headers? – Nakul Oct 17 '18 at 08:40
  • 3
    CollectionView scrolls to top whenever the size changes – Sajith Apr 16 '19 at 19:29
  • @Nakul I am facing the same issue with section headers in iOS 12. – Carl May 12 '19 at 10:26
  • This is definitely "the closest yet" but yeah, it isn't really "the right way" - headers etc don't work. – Fattie Jul 04 '19 at 15:14
  • 1
    @Fattie I've updated my answer for Swift 5.1 and iOS 13. Using Compositional Layout objects, things are now easier than ever. – Imanou Petit Jul 12 '19 at 04:54
  • for iOS12 the scrolling doesn't work as expected. and header view is not showing. any suggestion? – tara tandel Sep 10 '19 at 15:34
  • Remember that if you are using a collection with contentInset you should also subtract it before setting the layoutAttributes.bounds.size.width – MarMass Sep 24 '19 at 06:19
  • 1
    @ImanouPetit , i am going to raise a further interesting issue. What if you want to do it with a "real" UICollectionViewLayout ***that is to say NOT using a flow layout***. ! I wonder? – Fattie Oct 05 '19 at 15:19
  • 1
    I used the Solution for iOS12 (in Xcode11/iOS13) and had to add setNeedsLayout above layoutIfNeeded in the preferredLayoutAttributesFitting() method. Without this some of my auto layout constraints where not being updated on resize of the cell. – Brett Oct 06 '19 at 03:42
  • 1
    Hi, I'm trying this. Even though I'm setting the layout in the viewdidload as you did above, I get the crash stating that the collectionview must be initialized with a non-nil layout parameter. Is there something I'm missing? This is Xcode 11 for iOS 13. – geistmate Nov 28 '19 at 09:14
  • @geistmate The sample code is for a Storyboard project. In your Storyboard, delete the current root scene, replace it with a `UICollectionViewController` scene and set it as an instance of `CollectionViewController`. – Imanou Petit Nov 28 '19 at 09:50
  • In this solution Collection view automatically scrolls to top when i change the bottom content inset. It is reseting the contentoffset to (0,0). Can you please help me on this? @Sajith did you get any solution? – Bishow Gurung Dec 23 '19 at 18:54
  • using the iOS13 approach i am running into the issue that the height is right for the cells, visible during load, however, scrolling down, all cells share the same height from the estimated value... any idea? – Peter Lapisu Jun 01 '20 at 09:21
  • @ImanouPetit Does this answer apply to CollectionView created programatically? – schinj Jun 21 '20 at 17:14
  • Really detailed answer, thanks! But is there a way to specify full-width at design time, something like ```match_parent``` in Android's XML layout? Also how would you handle the situation when the label is statically mapped to the storyboard via ```@IBOutlet```? – Rosen Dimov Aug 23 '20 at 16:37
  • Hello!! I have used the second implementation but the `super.layoutAttributesForItem(at: indexPath)` returns height of value `50.0` the first time it is being executed. Thank you!! – PV169 Nov 11 '20 at 14:24
  • 6
    I have problem with compositional layout, and had to fallback to flow layout. The problem is that my cell has a wide range of height, from 50 ~ 600. When I give an estimate too small, the cell(which also is a collectionview) is not grow to large enough size. If i give an estimate too high, then the potential for very large white space. I have literally pulled my hair off and found no way to over come this. – zrfrank Dec 02 '20 at 08:55
  • Nobody mentioned but `copy()` for attributes seems to be redundant. So the following part can be removed wherever it is used `?.copy() as? UICollectionViewLayoutAttributes` – emrepun Jan 27 '21 at 15:18
  • The answer is great, but the solution for iOS13+ is not ideal in my opinion - (probably it's a bug in CollectionView, or I don't know). It uses estimatedSize making it impossible to do an `accurate` scrollToItem() because UIKit doesn't know the item's position exactly because real frame != estimated. Also, if you set the estimated size too big, then on the first load of the screen, some cells might have wrong frames until the user scrolls the collection. Trying to guess the "right" estimated size is little annoying :) – surfrider Feb 08 '21 at 08:40
  • All this does not fully work,since reloadData causes content offset to scroll to point to (x: 0, y: 0) – b.zdybowicz Feb 15 '21 at 19:16
  • This solution works well with iPhone, with one issue is that it scrolls to top when the layout changes. How I use the same solution to show two columns in iPad instead of one in iPhone. I tried to multiply the width in the `CustomFlowLayout.layoutAttributesForItem` like `layoutAttributes.frame.size.width = (collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right) / 2` but I always get one column. Any ideas? – Mostafa Al Belliehy May 05 '21 at 16:31
  • When you first launch your app, the cell shows with a height that you specified with your estimatedHeight until you scroll down and then the cell shows with the correct (adjusted) height. Wonder why this issue occurs after following your tutorial for iOS 13+ with compositional layout? Calling `collectionView.collectionViewLayout.InvalidateLayout()` did not solve the problem. @ImanouPetit – Junsu Kim Jul 06 '21 at 08:51
  • @JunsuKim did you manage to solve your issue? – Dejan Skledar Sep 21 '21 at 12:40
  • @DejanSkledar it remains unsolved. I resorted to using table view's .automaticDimension which works perfectly. Are you experiencing the same problem? – Junsu Kim Sep 22 '21 at 06:58
  • @JunsuKim I pinpointed the issue to views with multiple multiline labels.. when using only 1 label, it works ok... didnt find a workaround though. – Dejan Skledar Sep 22 '21 at 07:19
  • I successfully implement Solution 1 and it is working fine in iOS 13+, but I do the same thing in iOS 11 and 12 but Solution 2 isn't been returned the height of uicollectionview cell. I've tried to implement every possibility to do it, but nothing is working. My uicollectionviewcell has vertical uiscrollview. – Syed Bilal Dec 16 '21 at 19:28
  • To get dynamic size adjustment you need to override `preferredLayoutAttributesFitting` in UICollectionViewCell. Set the size you want on the layoutAttributes that get passed in. Compositional CollectionViewLayout will use these instead of the estimated value. – Chris Wood Oct 16 '22 at 19:51
32

Problem

You are looking for automatic height and also want to have full in width, it is not possible to get both in using UICollectionViewFlowLayoutAutomaticSize.

You want to do using UICollectionView so below is the solution for you.

Solution

Step-I: Calculate the expected height of Cell

1. If you have only UILabel in CollectionViewCell than set the numberOfLines=0 and that calculated the expected height of UIlable, pass the all three paramters

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. If your CollectionViewCell contains only UIImageView and if it's is supposed to be dynamic in Height than you need to get the height of UIImage (your UIImageView must have AspectRatio constraints)

// this will give you the height of your Image
let heightInPoints = image.size.height
let heightInPixels = heightInPoints * image.scale

3. If it contains both than calculated their height and add them together.

STEP-II: Return the Size of CollectionViewCell

1. Add UICollectionViewDelegateFlowLayout delegate in your viewController

2. Implement the delegate method

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

I hope this will help you out.

Fattie
  • 27,874
  • 70
  • 431
  • 719
Dhiru
  • 3,040
  • 3
  • 25
  • 69
  • 1
    Is it possible to have a horizontal scrolling UICollectionView with cells height increasing vertically with auto layout? – nr5 Sep 07 '17 at 05:38
  • I want to accomplish same but half width and dynamic height – Mihir Mehta Jul 27 '18 at 05:44
  • `return CGSize(width: view.frame.width/2, height: expectedHeight)` also keep the Padding in account than return the width otherwise two cell wouldn't fit in single row . – Dhiru Jul 27 '18 at 07:08
  • @nr5 Did you get the solution? My requirement is same as yours. Thanks. – Maulik Bhuptani Oct 18 '18 at 18:57
  • @MaulikBhuptani I could not do it completely with Auto Layout. Had to create a custom Layout class and override some class methods. – nr5 Oct 20 '18 at 04:17
  • @nr5 Thank you for the response. I am doing all the calculations at runtime and set the height accordingly. lol. – Maulik Bhuptani Oct 20 '18 at 05:29
  • heightForLabel function is returning me height more than what is really needed. :( The extra height is directly proportional to the text content. – Alen Alexander Aug 15 '20 at 13:56
23

There are a couple of ways you could tackle this problem.

One way is you can give the collection view flow layout an estimated size and use systemLayoutSizeFitting to calculate the cell size.

Note: As mentioned in the comments below, as of iOS 10 you no longer need to provide an estimated size to trigger the call to func preferredLayoutAttributesFitting(_ layoutAttributes:) on the cell. Previously (iOS 9) would require you to provide an estimated size if you wanted preferredLayoutAttributes to be called.

(assuming you are using storyboards and the collection view is connected via IB)

override func viewDidLoad() {
    super.viewDidLoad()
    let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    layout?.estimatedItemSize = CGSize(width: 375, height: 200) // your average cell size
}

For simple cells that will usually be enough. If the size is still incorrect, in the collection view cell you can override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes, which will give you more fine-grain control over the cell size. Note: You will still need to give the flow layout an estimated size.

Then override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes to return the correct size.

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
}

Alternatively, instead, you can use a sizing cell to calculate the size of the cell in the UICollectionViewDelegateFlowLayout. If you use this method consider caching the size for performance.

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

While these are not perfect production-ready examples, they should get you started in the right direction. I can not say this is the best practice, but this works for me, even with fairly complex cells containing multiple labels, that may or may not wrap to multiple lines.

Eric Murphey
  • 1,445
  • 15
  • 18
  • @Fattie Unclear how 'esitmatedItemSize' is irrelevant to a dynamically sized collection view cell, so I would like to hear your thoughts. (not sarcasm) – Eric Murphey Jun 05 '17 at 03:39
  • Additionally if this answer is missing something specific to be helpful and/or what you consider canonical, let me know. I won't argue the bounty, I would just like to be helpful to anyone else who views this question. – Eric Murphey Jun 05 '17 at 03:55
  • 1
    (including or not including estimatedItemSize makes no difference in current iOS, and is no help at all, either way, in the "full width + dynamic height" issue.) the code at the end, nibs are rarely used these days and it has little relevance to a dynamic height (from, example, a few dynamic height text views). thanks though – Fattie Jun 05 '17 at 13:11
  • 1
    I arrived at this solution as well, but in my case, iOS 11, Xcode 9.2, the cell contents seemed disconnected from the cell itself - I could not get an image to expand to the full height or width of the fixed dimension. I further noticed that CV was settings constraints in `preferredLayoutAttributesFitting`. I then added a constraint of my own in said function, binding to the fixed dimension from estimatedItemSize, FWIW, see my answer below. – Chris Conover Dec 15 '17 at 04:02
  • `preferredLayoutAttributesFitting` didn't work for me but `func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize` works – Gargo Jul 21 '23 at 14:51
18

I found a pretty easy solution for that issue: Inside of my CollectionViewCell I got a UIView() which is actually just a background. To get full width I just set the following Anchors

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

The "magic" happens in the first line. I set the widthAnchor dynamically to the width of the screen. Also important is to subtract the insets of your CollectionView. Otherwise the cell won't show up. If you don't want to have such a background view, just make it invisible.

The FlowLayout uses the following settings

layout.itemSize = UICollectionViewFlowLayoutAutomaticSize
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

Result is a full width sized cell with dynamic height.

enter image description here

inf1783
  • 944
  • 11
  • 12
  • 1
    just TBC @inf1783, you're doing this in UICollectionView ? (not table view) - correct? – Fattie Feb 08 '18 at 12:50
  • 1
    @Fattie Yes, it's a UICollectionView ;) – inf1783 Feb 08 '18 at 13:08
  • fantastic @inf1783, can't wait to try this out. By the way, another good way to do that is to subclass the UIView and simply add an intrinsic width (I'm guessing it would have the same effect, and it would probably make it work at storyboard time also) – Fattie Feb 08 '18 at 14:36
  • 1
    Every time when I try this I end up in endless loop with The behavior of the UICollectionViewFlowLayout is not defined error :/ – Skodik.o Mar 14 '18 at 11:07
  • This approach is prone to errors if you ever set an inset, it will throw the cell too wide for contents, and generally be brittle. – Chris Conover Jun 20 '18 at 04:24
  • Really nice solution but when I use it I first see that all cells are stretched beyond the screen and when I just start to scroll it all suddenly becomes stretched to the actual screen width. So it partially works and I guess when it comes to scale, we could have even more weird issues like that. IMO, not recommended. – OhadM Jun 06 '19 at 09:09
  • This answer is simple and achieves the goal: https://stackoverflow.com/a/57116115/7128177 – Mostafa Al Belliehy May 06 '21 at 16:00
13

WORKING!!! Tested on IOS:12.1 Swift 4.1

I have a very simple solution that just works with no constraint breaking.

enter image description here

My ViewControllerClass

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!

    let cellId = "CustomCell"

    var source = ["nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,","nomu", "when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. ", "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by", "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia,"]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        self.collectionView.register(UINib.init(nibName: cellId, bundle: nil), forCellWithReuseIdentifier: cellId)

        if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        }

    }

}


extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {

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

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as? CustomCell else { return UICollectionViewCell() }
        cell.setData(data: source[indexPath.item])
        return cell
    }


}

CustomCell class:

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

}

Main ingredient is the systemLayoutSizeFitting function in Customcell. And also we have to set width of the view inside Cell with constraints.

Abhishek Biswas
  • 1,125
  • 1
  • 13
  • 19
  • 1
    I think this is the best and simplest solution. However, I did not add a width constraint to the cell's view. I wanted it to change according to device width, so I tweaked `preferredLayoutAttributesFitting` method to achieve the. – Mostafa Al Belliehy May 06 '21 at 16:00
13

If you're using iOS 14 or newer, then you can use the UICollectionLayoutListConfiguration API, which makes it possible to use UICollectionView as a single column tableview, including features like horizontal swipe context menus and auto-height cells.

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

More info, including an example project, about how to configure cells and the data source can be found in this article from Apple: Implementing Modern Collection Views, especially the section that starts with Create a Simple List Layout.

The example projects contains a controller named ConferenceNewsFeedViewController which shows how to configure auto-height cells based on auto-layout.

Vignesh
  • 3,571
  • 6
  • 28
  • 44
Ely
  • 8,259
  • 1
  • 54
  • 67
7
  1. Set estimatedItemSize of your flow layout:

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

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

Full example

Tested on iOS 11.

Manuco Bianco
  • 124
  • 1
  • 9
Roman
  • 216
  • 3
  • 8
6

You have to add width constraint to CollectionViewCell

class SelfSizingCell: UICollectionViewCell {

  override func awakeFromNib() {
      super.awakeFromNib()
      contentView.translatesAutoresizingMaskIntoConstraints = false
      contentView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
  }
}
Mecid
  • 4,491
  • 6
  • 30
  • 30
4

Per my comment on Eric's answer, my solution is very similar to his, but I had to add a constraint in preferredSizeFor... in order to constrain to the fixed dimension.

    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
    }

This question has a number of duplicates, I answered it in detail here, and provided a working sample app here.

Manuco Bianco
  • 124
  • 1
  • 9
Chris Conover
  • 8,889
  • 5
  • 52
  • 68
3

Personally I found the best ways to have a UICollectionView where AutoLayout determines the size while each Cell can have a different size is to implement the UICollectionViewDelegateFlowLayout sizeForItemAtIndexPath function while using an actual Cell to measure the size.

I talked about this in one of my blog posts

Hopefully this one will help you to achieve what you want. I'm not 100% sure but I believe unlike UITableView where you can actually have a fully automatic height of cells by using AutoLayout inconjunction with

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44

UICollectionView does not have such a way of letting AutoLayout determine the size because UICollectionViewCell does not necessarily fills the whole width of the screen.

But here is a question for you: If you need full screen width cells, why do you even bother using the UICollectionView over a good old UITableView which comes with the auto sizing cells?

Manuco Bianco
  • 124
  • 1
  • 9
xxtesaxx
  • 6,175
  • 2
  • 31
  • 50
  • hold it, Aprit below is saying that UICollectionViewFlowLayoutAutomaticSize is now available in flow layout, in iOS10 ... – Fattie Jun 05 '17 at 13:26
  • I also just saw this. Apparently thats a thing now in iOS 10. I just watched the corresponding WWDC talk on this: https://developer.apple.com/videos/play/wwdc2016/219/ However, this will completely autosize the cell to fit its content. I'm not sure how you could tell the cell (with AutoLayout) to fill the screen width since you cannot setup a constraint between the UICollectionViewCell and its parent (at least not in StoryBoards) – xxtesaxx Jun 05 '17 at 16:15
  • According to the talk, you still could overwrite sizeThatFits() or preferredLayoutAttributesFitting() and calculate the size by yourself but thats not what the OP asked for I think. Anyways, I would still be interested in the use case for when he/she needs a full width UICollectionViewCell and why it would be such a big deal to not use a UITableView in this particular case (sure there can be certain cases but then I guess you just have to deal with doing some calculations yourself) – xxtesaxx Jun 05 '17 at 16:56
  • thinking about it, it's quite incredible that you can't simply set "columns == 1" (and then perhaps columns == 2 when the device is sideways), with collection views. – Fattie Jun 05 '17 at 17:38
  • Well you can write a custom layout to do this. Its actually not that complicated and flow layout does a good job in a lot of cases. We cannot expect apple to cover every case and this would surely cause some conflicts in other places. You basically can achieve a 1/2 column layout simply by overwriting sizeForItemAt together with a sizing cell as I described it in my blog post. Thats really simple and only a few lines of code are needed. – xxtesaxx Jun 05 '17 at 17:46
2

Not sure if this qualifies as a "really good answer", but it's what I'm using to accomplish this. My flow layout is horizontal, and I'm trying to make the width adjust with autolayout, so it's similar to your situation.

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

Then in the view controller where the autolayout constraint gets modified, I fire off an NSNotification.

NotificationCenter.default.post(name: NSNotification.Name("constraintMoved"), object: self, userInfo: nil)

In my UICollectionView subclass, I listen for that notification:

// viewDidLoad
NotificationCenter.default.addObserver(self, selector: #selector(handleConstraintNotification(notification:)), name: NSNotification.Name("constraintMoved"), object: nil)

and invalidate the layout:

func handleConstraintNotification(notification: Notification) {
    self.collectionView?.collectionViewLayout.invalidateLayout()
}

This causes sizeForItemAt to be called again using the collection view's new size. In your case, it should be able to update given the new constraints available in the layout.

Mark Suman
  • 10,430
  • 2
  • 27
  • 23
  • just TBC Mark do you mean you're actually *changing* the size of a cell? (ie, the cell appears and it is size X, then something happens in your app and it becomes size Y ..?) thx – Fattie Jun 05 '17 at 13:18
  • Yes. When the user drags an element on the screen, an event fires that updates the size of the cell. – Mark Suman Jun 09 '17 at 03:15
2

The correct modern solution in 2023 is to use a compositional layout, which has been documented in various answers on this post.

For strong documentation on any plausible example, consult this guide by Apple with associated source code:

https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views

Please note that the estimated size should neither be too small or too large otherwise it'll look a bit weird on load.

When reloading layout or switching orientation, invalidate the layout using:

self.collectionView.collectionViewLayout.invalidateLayout()

Other Options

It is also possible to do it with preferredLayoutAttributesFitting after updating cells and invalidating the content size but, this leads to a very janky solution that is not fluid nor premium.

Strongly recommend you follow an example in this post or on Apple's link.

odlh
  • 431
  • 5
  • 6
1

On your viewDidLayoutSubviews, set the estimatedItemSize to full width (layout refers to the UICollectionViewFlowLayout object):

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

On your cell, be sure that your constraints touch both the top and bottom of the cell (the following code uses Cartography to simplify setting the constraints but you can do it with NSLayoutConstraint or IB if you want):

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
    }

Voila, you cells will now autogrow in height!

Raphael
  • 7,972
  • 14
  • 62
  • 83
1

AutoLayout can be used for auto-sizing of cells in CollectionView in 2 easy steps:

  1. Enabling dynamic cell sizing

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. Have a container view and set the containerView.widthAnchor.constraint from collectionView(:cellForItemAt:)to limit the width of contentView to width of 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
        }
    }

    ....
}

That's it you'll get the desired result. Refer following gists for full code:

Reference/Credits :

Screenshot: enter image description here

Alex
  • 448
  • 7
  • 14
1

I also faced with dynamic cell's height issue and could resolve the issue using Autolayout and manual height calculation. No need to use estimated sizes, instantiating or creating cells.

Also this solution provides handling of multiline labels. It's based on calculating of subviews height of all subviews in a cell.

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 uses size of the cell which is calculated in getProductHeightForWidth method:

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

I created a story here: https://volodymyrrykhva.medium.com/uicollectionview-cells-with-dynamic-height-using-autolayout-a4e346b7bd2a

Full code on solution is on GitHub: https://github.com/ascentman/DynamicHeightCells

Volodymyr
  • 1,192
  • 21
  • 42
1

I have seen many complex answers for cells with dynamic height. However after few google searches i found this simple answer.

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
}

Important Part

collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

LabelOnlyCell class for reference

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
}

}

Screenshot for reference. enter image description here

Irtaza fayaz
  • 383
  • 4
  • 11
1

When using UICollectionViewCompositionalLayout make sure you set both itemSize and groupSize height to .estimated:

My problem was that I was using .fractional(1) for item height and .estimated(44) for group and it didn't work.

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

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


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

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

Nik
  • 9,063
  • 7
  • 66
  • 81
0

None of the solutions were working for me as I need dynamic width to adapt between iPhones width.

    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()
        }
    }
iOS Flow
  • 69
  • 1
  • 8
0

I followed this answer, simple and achieves the goal. Plus it gives me the benefit to adjust my view for iPad unlike the selected answer.

However I wanted the width of the cell to change according to device width, so I did not add a width constraint, and tweaked the preferredLayoutAttributesFitting method to be like this:

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

From iOS 10, we've got new API on flow layout to do that.

All you have to do is set your flowLayout.estimatedItemSize to a new constant, UICollectionViewFlowLayoutAutomaticSize.

Source

Manuco Bianco
  • 124
  • 1
  • 9
Arpit Dongre
  • 1,683
  • 19
  • 30
-2

My solution is lifted from https://www.advancedswift.com/autosizing-full-width-cells/

Add the following inside your Custom Cell Class:

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

And the following in the ViewDidLoad of the ViewController containing your Collection View:

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

This is the simplest/shortest solution I've found.

iOS_Mouse
  • 754
  • 7
  • 13
  • ??????? this is just setting the cell width to 200. – Fattie Oct 23 '21 at 14:56
  • Also, do note that article seems to have many problems. The only thing `systemLayoutSizeFitting` does is guess a size which you may need to know (I don't think the other cell-drawing parts of iOS even use it). Note that it says clearly in the apple doco for that call "This method does not actually change the size of the view." – Fattie Oct 23 '21 at 14:58