222

I'm trying to get self sizing UICollectionViewCells working with Auto Layout, but I can't seem to get the cells to size themselves to the content. I'm having trouble understanding how the cell's size is updated from the contents of what's inside the cell's contentView.

Here's the setup I've tried:

  • Custom UICollectionViewCell with a UITextView in its contentView.
  • Scrolling for the UITextView is disabled.
  • The contentView's horizontal constraint is: "H:|[_textView(320)]", i.e. the UITextView is pinned to the left of the cell with an explicit width of 320.
  • The contentView's vertical constraint is: "V:|-0-[_textView]", i.e. the UITextView pinned to the top of the cell.
  • The UITextView has a height constraint set to a constant which the UITextView reports will fit the text.

Here's what it looks like with the cell background set to red, and the UITextView background set to Blue: cell background red, UITextView background blue

I put the project that I've been playing with on GitHub here.

Rishil Patel
  • 1,938
  • 3
  • 11
  • 28
rawbee
  • 2,756
  • 3
  • 13
  • 21
  • Are you implementing the sizeForItemAtIndexPath method? – Daniel Galasko Sep 17 '14 at 16:04
  • @DanielGalasko I'm not. My understanding is that the new self sizing cells feature in iOS 8 doesn't require you to do that any more. – rawbee Sep 17 '14 at 16:10
  • not sure where you got that info from but you still need to, take a look at the WWDC excerpt http://asciiwwdc.com/2014/sessions/226 – Daniel Galasko Sep 17 '14 at 16:13
  • but again it depends on your use case, make sure you implement estimatedItemSize – Daniel Galasko Sep 17 '14 at 16:15
  • @DanielGalasko That same video is what made me think that. "If you want to use self-sizing cells in UICollectionViewFlowLayout, it's one line. ...I replaced ItemSize with estimatedItemSize and that's it." – rawbee Sep 17 '14 at 16:22
  • But since you are using a text view and you are updating the text on the fly you are going to have to implement the sizeThatFits method in your cell no? – Daniel Galasko Sep 17 '14 at 16:35
  • I have answered your question and have it working on my machine, let me know if you want a github sample – Daniel Galasko Sep 17 '14 at 17:04
  • 1
    **2019 - It's essential to go look at this: https://stackoverflow.com/a/58108897/294884** – Fattie Sep 26 '19 at 02:47

16 Answers16

332

Updated for Swift 5

preferredLayoutAttributesFittingAttributes renamed to preferredLayoutAttributesFitting and use auto sizing


Updated for Swift 4

systemLayoutSizeFittingSize renamed to systemLayoutSizeFitting


Updated for iOS 9

After seeing my GitHub solution break under iOS 9 I finally got the time to investigate the issue fully. I have now updated the repo to include several examples of different configurations for self sizing cells. My conclusion is that self sizing cells are great in theory but messy in practice. A word of caution when proceeding with self sizing cells.

TL;DR

Check out my GitHub project


Self sizing cells are only supported with flow layout so make sure thats what you are using.

There are two things you need to setup for self sizing cells to work.

1. Set estimatedItemSize on UICollectionViewFlowLayout

Flow layout will become dynamic in nature once you set the estimatedItemSize property.

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

2. Add support for sizing on your cell subclass

This comes in 2 flavours; Auto-Layout or custom override of preferredLayoutAttributesFittingAttributes.

Create and configure cells with Auto Layout

I won't go to in to detail about this as there's a brilliant SO post about configuring constraints for a cell. Just be wary that Xcode 6 broke a bunch of stuff with iOS 7 so, if you support iOS 7, you will need to do stuff like ensure the autoresizingMask is set on the cell's contentView and that the contentView's bounds is set as the cell's bounds when the cell is loaded (i.e. awakeFromNib).

Things you do need to be aware of is that your cell needs to be more seriously constrained than a Table View Cell. For instance, if you want your width to be dynamic then your cell needs a height constraint. Likewise, if you want the height to be dynamic then you will need a width constraint to your cell.

Implement preferredLayoutAttributesFittingAttributes in your custom cell

When this function is called your view has already been configured with content (i.e. cellForItem has been called). Assuming your constraints have been appropriately set you could have an implementation like this:

//forces the system to do one layout pass
var isHeightCalculated: Bool = false

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    //Exhibit A - We need to cache our calculation to prevent a crash.
    if !isHeightCalculated {
        setNeedsLayout()
        layoutIfNeeded()
        let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
        var newFrame = layoutAttributes.frame
        newFrame.size.width = CGFloat(ceilf(Float(size.width)))
        layoutAttributes.frame = newFrame
        isHeightCalculated = true
    }
    return layoutAttributes
}

NOTE On iOS 9 the behaviour changed a bit that could cause crashes on your implementation if you are not careful (See more here). When you implement preferredLayoutAttributesFittingAttributes you need to ensure that you only change the frame of your layout attributes once. If you don't do this the layout will call your implementation indefinitely and eventually crash. One solution is to cache the calculated size in your cell and invalidate this anytime you reuse the cell or change its content as I have done with the isHeightCalculated property.

Experience your layout

At this point you should have 'functioning' dynamic cells in your collectionView. I haven't yet found the out-of-the box solution sufficient during my tests so feel free to comment if you have. It still feels like UITableView wins the battle for dynamic sizing IMHO.

Caveats

Be very mindful that if you are using prototype cells to calculate the estimatedItemSize - this will break if your XIB uses size classes. The reason for this is that when you load your cell from a XIB its size class will be configured with Undefined. This will only be broken on iOS 8 and up since on iOS 7 the size class will be loaded based on the device (iPad = Regular-Any, iPhone = Compact-Any). You can either set the estimatedItemSize without loading the XIB, or you can load the cell from the XIB, add it to the collectionView (this will set the traitCollection), perform the layout, and then remove it from the superview. Alternatively you could also make your cell override the traitCollection getter and return the appropriate traits. It's up to you.

Let me know if I missed anything, hope I helped and good luck coding


Alex
  • 619
  • 1
  • 7
  • 21
Daniel Galasko
  • 21,827
  • 7
  • 71
  • 93
  • 2
    I'm trying to implement the same thing with flow layout, however when I return it newFrame with updated size, the layout doesn't seem to recalculate y positions, which are still based on the height in estimatedSize. What's missing? – anna Oct 06 '14 at 21:51
  • @anna are you calling invalidateLayout on the layout? – Daniel Galasko Oct 07 '14 at 04:31
  • @anna my apologies I thought you were changing your layout and the view was not updating. Is your - (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes being called? – Daniel Galasko Oct 07 '14 at 19:39
  • yes, it's being called and retuning the correct height for each cell but the contentSize of the flow layout never adjusts to accommodate the new heights. – anna Oct 07 '14 at 19:46
  • Where are you setting the estimated size property on the collection view layout? – Daniel Galasko Oct 07 '14 at 20:06
  • Yep, setting it on the layout. I've seen other comments in Apple forums where people had that problem. But not sure if it's a bug or you have a way to do it if it's working for you? – anna Oct 07 '14 at 20:08
  • I'm assuming you started with a brand new project with a single collection view and you testing with that? I can send you a sample project in the morning?:) – Daniel Galasko Oct 07 '14 at 20:16
  • Correct, brand new project. Sure, if you want to put up a sample on GitHub or something, would be great! – anna Oct 07 '14 at 20:22
  • @anna give this a try https://github.com/danielgalasko/DGSelfSizingCollectionViewCells/tree/master – Daniel Galasko Oct 08 '14 at 07:50
  • Thanks Daniel. One difference is that you're using Storyboard and I wasn't, wonder what extra setting does that add. But even so, in your project there's still weird behavior such that all the cells don't show up until scrolling begins and the last cell is off. Makes me suspect the flow layout is not well implemented for self-sizing :S – anna Oct 08 '14 at 13:29
  • @anna I also noticed the odd disappearing cells but i think thats a bug with UITextView because when i debugged the views there was a strange image embedded inside the text view. You should be able to use it without a storyboard – Daniel Galasko Oct 08 '14 at 13:32
  • 1
    Ah, found the difference, you set the estimated height pretty high (400) whereas I was doing the minimum (44). That helped. But the contentSize still seems to not be getting set correctly until you scroll all that way through, so still not very usable. Btw, I changed UITextView to UILabel, same behaviour. – anna Oct 08 '14 at 14:15
  • @anna what a pity that it is running so averagely! At least you somewhere though?:) – Daniel Galasko Oct 08 '14 at 14:32
  • Yeah, thanks for helping to solve part of the mystery! But I don't think the flow layout is ready enough to use in production. Probably will stick with the old methods a while longer.. – anna Oct 08 '14 at 15:18
  • I have also tried for many hours to get dynamic sized cells in a flow layout to work and they work about 90% of the time. But some of the time cells are missing or in the wrong place, and if you scroll around the collection view sometimes the missing cells will appear or disappear. A lot of the time the missing cells are at the end of the view. Changing the estimatedSize makes a huge difference in how things work and how fast the view reloads, contrary to the doc, which implies it's just an optimization. My conclusion is that it's just too flakey to be useful beyond the most simple examples. – LenK Dec 04 '14 at 01:09
  • The wwdc 2014 "What's new in Table Views and Collection Views" talks about contentSizeAdjustment and contentSizeOffset to handle the disappearing cells, content size mis-match, offset issues, and scrolling bumpy-ness. However, it doesn't provide clear example on how to use those two variables. Hope this leads to solution :) – Ramesh Feb 03 '15 at 04:16
  • I've tried everything in this answer (and beyond) but still couldn't make it to work. – Can Poyrazoğlu Jun 08 '15 at 12:18
  • @CanPoyrazoğlu try checkout my github project and build on that:) – Daniel Galasko Jun 08 '15 at 12:33
  • @DanielGalasko I'm looking, but my collection view usually needs to look like table view, and as you've mentioned, it doesn't work right out of box for scenario. – Can Poyrazoğlu Jun 08 '15 at 12:35
  • @CanPoyrazoğlu yes thats definitely one of the biggest limitations. its quite tricky to get working, i posted a question here about it http://stackoverflow.com/questions/28670951/uicollectionviewcell-systemlayoutsizefittingsize-returns-incorrect-width – Daniel Galasko Jun 08 '15 at 12:36
  • @DanielGalasko I'll let you know if I find a solution. – Can Poyrazoğlu Jun 08 '15 at 12:37
  • @DanielGalasko what if you have more than one xib for the cells? meaning three different layout cells? how would you implement the "calculateStandAlonecellHeight"? I tried everything in here except for the "calculateStandAloneCellHeight" the problem is if I "reloadData" in "Landscape" orientation and then switch back to portrait, somehow the new cells that appear after this rotation have a different size than what the "preferedLayoutAttributesFittingAttributes" return – Shabarinath Pabba Jun 12 '15 at 22:26
  • 1: you need to make sure that your layout is invalidated when the bounds changes. Flow layout has an override for this. The same way you know what cell to dequeue in cellForRow is how you know what cell to use to calculate the height @ShabarinathPabba – Daniel Galasko Jun 13 '15 at 07:46
  • I have a customcollectionview cell and this [self.contentView systemLayoutSizeFittingSize:(UILayoutFittingCompressedSize)].height is returning zero always. I am doing all the steps that has been suggested. – Sana Jun 22 '15 at 20:49
  • @Sana then your constraints are most likely wrong. Please see my link about configuring constraints on a cell:) – Daniel Galasko Jun 23 '15 at 05:31
  • I was just testing UICollectionView with dynamic type and autosizing cells is enabler for such type of functionality. Unfortunately it doesn't work correctly. Layout changes after cells are visible. This is also visible in demo provided. When data source provides more strings that are longer, result is completely broken. This is a correct answer how it should be done, but unfortunately UICollectionFlow layout autosizing is broken and can not be used in production :( This doesn't work correctly yet even on iOS9 beta5. Pity. – Juraj Antas Aug 18 '15 at 15:06
  • @JurajAntas are you invalidating your layout? I haven't had any issues. Perhaps ask a new question? – Daniel Galasko Aug 19 '15 at 12:33
  • @DanielGalasko Not sure what you are referring to, but your own demo is not working correctly. Used xcode7 beta5 and running on iOS8.4.1. Result produced is not satisfactory at all. Here is screen recording: https://youtu.be/2EQHgM6EdKA – Juraj Antas Aug 19 '15 at 12:50
  • 19
    This appears to be fundamentally broken on iOS 9 GM. Setting `estimatedItemSize` leads to crashes -- there seems to be some huge bug in how auto layout is trying to process the UICollectionViewCell. – Wes Campaigne Sep 15 '15 at 23:45
  • 4
    Running into a similar issue, has anybody found a work around? – Tony Oct 10 '15 at 06:19
  • @WesCampaigne I updated my Github, your input would be appreciated. cc – Daniel Galasko Feb 16 '16 at 19:23
  • I fixed this in ios 9 doing the following: no preferredLayoutAttributesFittingAttributes. Created my cell using initWithFrame (so no XIB/storyboard) – João Nunes Apr 05 '16 at 13:09
  • @JoãoNunes that doesn't make sense. I never advocated storyboards or XIBs. What was broken on iOS 9? I would welcome a PR on GitHub if you feel it was broken – Daniel Galasko Apr 05 '16 at 13:14
  • 2
    your solution as others say does not work always. Try changing the constraints of a cell after selection and deselection for ex. The whole collection breaks again. The Auto sizing is broken on ios9! I will revert back to use itemSize delegate and do the calculations myself. – João Nunes Apr 05 '16 at 14:00
  • 1
    @JoãoNunes completely agree. It's impossible to get it working so I also use normal delegate size calculation. I thought you managed to get estimatedItemSize working. I know if you change the layout as well it crashes – Daniel Galasko Apr 05 '16 at 14:06
  • @DanielGalasko Check out my answer below. I wasn't originally able to comment against your answer but mine is a lot simpler, I recommend editing yours. – Matt Koala Jun 03 '16 at 19:39
  • Thanks for the post! I tried it, but ran into problems with a simple example. I think [we might need to set `contentView.translatesAutoresizingMaskToConstraints = false`](http://stackoverflow.com/q/38193314/35690). – Senseful Jul 04 '16 at 23:31
  • @Senseful I am so close to turning this post into a Wiki. It's really tough to maintain because it's so buggy and difficult to get working! – Daniel Galasko Jul 05 '16 at 04:53
  • I find that simply implementing a "preferredLayoutAttributesFittingAttributes" in my custom UICollectionViewCell that simply returns the layout parameter fixes sizing issues on reused cells. This effectively disables the default version of the function, if I include a call to the super version, again the sizing fails. – user3621075 Jul 07 '16 at 13:37
  • Is there is any way that the cells can take up empty spaces between them just like game of tetris, if they have different height and width among them? – Bista Sep 01 '16 at 11:42
  • Hi, this works well if my collectionView is static, but if I add some cells (and it won't fit on the screen), the last cells are displayed only when I scroll. If I invalidate the flow's layout in the viewWillLayoutSubviews, it works well on ios10, but infinite loop on ios9, if I invalidate the layout after I reload the collectionView, it works well on ios10, but breaks on ios9 after adding a few cells, any idea? – kennyevo Oct 25 '16 at 08:07
  • Is `preferredLayoutAttributesFittingAttributes` more performant than `Auto-Layout`? Ive tried an Auto Layout grid list before wiht varying item sizes and the performance was awful. Hoping to give your `preferredLayoutAttributesFittingAttributes` suggestion a try tomorrow and see what the results are. – James Heald Dec 01 '17 at 02:27
  • @JamesHeald I can't comment on performance as it depends on your implementation :) you will just need to profile it – Daniel Galasko Dec 05 '17 at 10:37
  • shouldn't we call super preferredLayoutAttributesFitting first? – boog Jun 13 '18 at 07:26
  • As of Xcode 10 and iOS 12, this only works when loading the first cells. I fixed it by removing the `isHeightCalculated` test. Seems to work now and no memory leak as I can tell. – Skoua May 24 '19 at 17:21
  • A BIG caveat here for me was that xibs use size classes. That breaks everything right off the bat. Had to implement my cell subclass entirelty in code – nteissler Sep 19 '19 at 19:27
  • One thing I noticed that's missing in your code. You're setting width of the newFrame, but not height. Since we're using systemLayoutSizeFitting to get the size it includes the height. Since we are talking about dynamic heights mostly, I think the code should be updated for future reference when other people run into this. – gngrwzrd Jun 03 '20 at 18:49
  • Why are the calls to setNeedsLayout and layoutIfNeeded required? They solved my problem and I wanted an explanation because I couldnt come up with it. If I didnt use them then systemLayoutSizeFitting is returning erroneous sizes. – Rohan Bhale Aug 05 '20 at 16:00
56

In iOS10 there is new constant called UICollectionViewFlowLayout.automaticSize (formerly UICollectionViewFlowLayoutAutomaticSize), so instead:

self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)

you can use this:

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

It has better performance especially when cells in your collection view have constant width.

Accessing Flow Layout:

override func viewDidLoad() {
   super.viewDidLoad()

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

Swift 5 Updated:

override func viewDidLoad() {
   super.viewDidLoad()

   if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
      flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }
}
shim
  • 7,170
  • 10
  • 62
  • 95
pawel221
  • 839
  • 7
  • 8
  • 9
    Where do you set this? in viewdidload? How do you get a handle on flowlayout? – UKDataGeek Apr 01 '17 at 10:31
  • 3
    I managed to implement this - but it has a strange behaviour that makes it center the cells if there is a single cell. Here is the stack overflow question about it - has me stumped: http://stackoverflow.com/questions/43266924/stop-single-uicollectionview-cell-flowing-to-the-centre-of-the-screen/43269736?noredirect=1 – UKDataGeek Apr 07 '17 at 07:38
  • 1
    You can access the flow layout via the collectionViews `collectionViewLayout` and just check that it is of type `UICollectionViewFlowLayout` – d4Rk Mar 19 '18 at 12:08
  • 8
    it makes my cells very small, useless – user924 Apr 14 '19 at 16:35
45

A few key changes to Daniel Galasko's answer fixed all my problems. Unfortunately, I don't have enough reputation to comment directly (yet).

In step 1, when using Auto Layout, simply add a single parent UIView to the cell. EVERYTHING inside the cell must be a subview of the parent. That answered all of my problems. While Xcode adds this for UITableViewCells automatically, it doesn't (but it should) for UICollectionViewCells. According to the docs:

To configure the appearance of your cell, add the views needed to present the data item’s content as subviews to the view in the contentView property. Do not directly add subviews to the cell itself.

Then skip step 3 entirely. It isn't needed.

shim
  • 7,170
  • 10
  • 62
  • 95
Matt Koala
  • 2,081
  • 2
  • 13
  • 14
  • 1
    Interesting; this is specifically for Xibs but nevertheless thanks, will try and muddle with the Github project and see if I can reproduce. Maybe divide the answer into Xib vs programmatic – Daniel Galasko Sep 02 '15 at 05:13
  • Very Helpful . Thanks! – ProblemSlover Sep 07 '15 at 17:10
  • 2
    iOS 9, Xcode 7. Cells are protoyped in storyboard, set custom subclass. Tried creating a property called `contentView`, Xcode complains it conflicts with existing property. Tried adding subviews to `self.contentView` and set constraints to that, then app crashes. – Nicolas Miari Oct 06 '15 at 11:06
  • @NicolasMiari I don't know about how to do this programmatically, my solution is really just to add a single uiview in the XIB where everything goes inside it. You don't need to create a property or anything. – Matt Koala Oct 07 '15 at 18:56
  • I'm facing the same problem as @NicolasMiari. When I set the estimatedContentSize for cells prototyped in storyboard with autolayout constraints on iOS 9 / Xcode 7 the app crashes with a bad access exception and no helpful stacktrace – wasabi Dec 06 '15 at 17:22
  • I'm not sure what the confusion is. I believe that @NicolasMiari wasn't following my instructions properly. You don't need a property "contentView". You don't need to "add subviews to self.contentView". In your prototyped cells, make sure everything is inside a single UIView. That's it. – Matt Koala Dec 07 '15 at 06:10
  • When you drag a view into the cell in IB, top level, you are effectively adding a subview to the cell's `contentView`, right? – Nicolas Miari Dec 07 '15 at 06:27
  • No. You aren't adding a subview to the `contentView`. I believe you are actually creating the `contentView` itself with the view I'm saying to add. – Matt Koala Dec 07 '15 at 07:11
  • Would love a link to a sample project to see this in action. Storyboard and programmatic would be great:) – Daniel Galasko Jun 04 '16 at 10:05
  • Based on what i'm seeing here adding the views directly to the cell in storyboard are adding it to the content view. I printed the contents of cell.contentView, cell.contentView.subviews, and cell.subviews and without having added a "parent view" to the cell, cell.subviews just has the contentView, and the contentView contains all of the subviews i added in storyboard. – Philippe Sabourin Jan 25 '17 at 18:44
  • @ProblemSlover I'm confused. Doesn't the cell itself have a `contentView`? Should we be adding it to that? Why do we need an extra view? – Honey Dec 04 '17 at 16:53
  • always add subviews to the `contentView` property of UICollectionViewCell and UITableViewCell – PatrickDotStar Aug 30 '19 at 12:34
34

In iOS 10+ this is a very simple 2 step process.

  1. Ensure that all your cell contents are placed within a single UIView (or inside a descendant of UIView like UIStackView which simplifies autolayout a lot). Just like with dynamically resizing UITableViewCells, the whole view hierarchy needs to have constraints configured, from the outermost container to the innermost view. That includes constraints between the UICollectionViewCell and the immediate childview

  2. Instruct the flowlayout of your UICollectionView to size automatically

    yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    
Marmoy
  • 7,684
  • 7
  • 41
  • 72
  • Im curious, how would wrapping your cells contents in a UIView make Auto Layout perform better? Id have thought it would perform better with a more shallow hierarchy? – James Heald Dec 01 '17 at 02:29
  • 2
    I did not mention performance, only simplicity. Self-sizing cells will only work if you have constraints spanning all the way between left and right, and between top and bottom. Achieving that is easiest when all views are wrapped in a single container. If that container is a UIStackView it is easier that if it is any other descendant of UIView. – Marmoy Dec 01 '17 at 08:40
  • Can you please tell me how to set height constant (like set height 25, and width will be automatically changed) for UICollectionViewFlowLayoutAutomaticSize. – Svetoslav Atanasov Jan 25 '19 at 14:58
  • Using Swift 4.1, Xcode tells me `UICollectionViewFlowLayout.automaticSize` has been renamed to `UICollectionViewFlowLayoutAutomaticSize`. – LinusGeffarth Feb 22 '19 at 17:03
14
  1. Add flowLayout on viewDidLoad()

    override func viewDidLoad() {
        super.viewDidLoad() 
        if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = CGSize(width: 1, height:1)
        }
    }
    
  2. Also, set an UIView as mainContainer for your cell and add all required views inside it.

  3. Refer to this awesome, mind-blowing tutorial for further reference: UICollectionView with autosizing cell using autolayout in iOS 9 & 10

dianakarenms
  • 2,220
  • 1
  • 18
  • 22
13

EDIT 11/19/19: For iOS 13, just use UICollectionViewCompositionalLayout with estimated heights. Don't waste your time dealing with this broken API.

After struggling with this for some time, I noticed that resizing does not work for UITextViews if you don't disable scrolling:

let textView = UITextView()
textView.scrollEnabled = false
noobular
  • 3,191
  • 2
  • 22
  • 19
6

contentView anchor mystery:

In one bizarre case this

    contentView.translatesAutoresizingMaskIntoConstraints = false

would not work. Added four explicit anchors to the contentView and it worked.

class AnnoyingCell: UICollectionViewCell {
    
    @IBOutlet var word: UILabel!
    
    override init(frame: CGRect) {
        super.init(frame: frame); common() }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder); common() }
    
    private func common() {
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            contentView.leftAnchor.constraint(equalTo: leftAnchor),
            contentView.rightAnchor.constraint(equalTo: rightAnchor),
            contentView.topAnchor.constraint(equalTo: topAnchor),
            contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
}

and as usual

    estimatedItemSize = UICollectionViewFlowLayout.automaticSize

in YourLayout: UICollectionViewFlowLayout

Who knows? Might help someone.

Credit

https://www.vadimbulavin.com/collection-view-cells-self-sizing/

stumbled on to the tip there - never saw it anywhere else in all the 1000s articles on this.

Community
  • 1
  • 1
Fattie
  • 30,632
  • 54
  • 336
  • 607
3

I did a dynamic cell height of collection view. Here is git hub repo.

And, dig out why preferredLayoutAttributesFittingAttributes is called more than once. Actually, it will be called at least 3 times.

The console log picture : enter image description here

1st preferredLayoutAttributesFittingAttributes:

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c290e0> index path: (<NSIndexPath:    0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 57.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);

The layoutAttributes.frame.size.height is current status 57.5.

2nd preferredLayoutAttributesFittingAttributes:

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c16370> index path: (<NSIndexPath: 0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);

The cell frame height changed to 534.5 as our expected. But, the collection view still zero height.

3rd preferredLayoutAttributesFittingAttributes:

(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa403d516a0> index path: (<NSIndexPath: 0xc000000000000016> 
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); 

(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 477);

You can see the collection view height was changed from 0 to 477.

The behavior is similar to handle scroll:

1. Before self-sizing cell

2. Validated self-sizing cell again after other cells recalculated.

3. Did changed self-sizing cell

At beginning, I thought this method only call once. So I coded as the following:

CGRect frame = layoutAttributes.frame;
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy];
newAttributes.frame = frame;
return newAttributes;

This line:

frame.size.height = frame.size.height + self.collectionView.contentSize.height;

will cause system call infinite loop and App crash.

Any size changed, it will validate all cells' preferredLayoutAttributesFittingAttributes again and again until every cells' positions (i.e frames) are no more change.

byJeevan
  • 3,240
  • 2
  • 33
  • 54
Yang Young
  • 376
  • 2
  • 6
3

The solution comprises 3 simple steps:

  1. Enabling dynamic cell sizing

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

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

    ....
}

Since you want to enable self-sizing of UITextView, it has an additional step to;

3. Calculate and set the heightAnchor.constant of UITextView.

So, whenever the width of contentView is set we'll adjust height of UITextView along in didSet of maxWidth.

Inside UICollectionViewCell:

var maxWidth: CGFloat? {
    didSet {
        guard let maxWidth = maxWidth else {
            return
        }
        containerViewWidthAnchor.constant = maxWidth
        containerViewWidthAnchor.isActive = true
        
        let sizeToFitIn = CGSize(width: maxWidth, height: CGFloat(MAXFLOAT))
        let newSize = self.textView.sizeThatFits(sizeToFitIn)
        self.textViewHeightContraint.constant = newSize.height
    }
}

These steps will get you the desired result.

Complete runnable gist

Reference: Vadim Bulavin blog post - Collection View Cells Self-Sizing: Step by Step Tutorial

Screenshot:

enter image description here

Alex
  • 133
  • 1
  • 6
2

In addition to above answers,

Just make sure you set estimatedItemSize property of UICollectionViewFlowLayout to some size and do not implement sizeForItem:atIndexPath delegate method.

That's it.

Murat Yasar
  • 876
  • 8
  • 22
1

If you implement UICollectionViewDelegateFlowLayout method:

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

When you call collectionview performBatchUpdates:completion:, the size height will use sizeForItemAtIndexPath instead of preferredLayoutAttributesFittingAttributes.

The rendering process of performBatchUpdates:completion will go through the method preferredLayoutAttributesFittingAttributes but it ignores your changes.

Yang Young
  • 376
  • 2
  • 6
  • I've seen this misbehavior, but only when the estimated size is zero. Are you sure you're setting an estimated size? – dgatwood Feb 21 '17 at 21:32
1

To whomever it may help,

I had that nasty crash if estimatedItemSize was set. Even if I returned 0 in numberOfItemsInSection. Therefore, the cells themselves and their auto-layout were not the cause of the crash... The collectionView just crashed, even when empty, just because estimatedItemSize was set for self-sizing.

In my case I reorganized my project, from a controller containing a collectionView to a collectionViewController, and it worked.

Go figure.

Jean Le Moignan
  • 21,119
  • 3
  • 29
  • 37
  • 1
    Try calling `collectionView.collectionViewLayout.invalidateLayout()` after `collectionView.reloadData()`. – chengsam Jun 27 '17 at 02:47
  • for ios10 and below add this code ` override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() if #available(iOS 11.0, *) { } else { mainCollectionView.collectionViewLayout.invalidateLayout() } }` – Marosdee Uma Mar 29 '18 at 08:33
1

For anyone who tried everything without luck, this is the only thing that got it working for me. For the multiline labels inside cell, try adding this magic line:

label.preferredMaxLayoutWidth = 200

More info: here

Cheers!

HelloimDarius
  • 645
  • 5
  • 20
0

The example method above does not compile. Here is a corrected version (but untested as to whether or not it works.)

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes 
{
    let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes

    var newFrame = attr.frame
    self.frame = newFrame

    self.setNeedsLayout()
    self.layoutIfNeeded()

    let desiredHeight: CGFloat = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    newFrame.size.height = desiredHeight
    attr.frame = newFrame
    return attr
}
P.J.Radadiya
  • 1,372
  • 10
  • 16
user3246173
  • 478
  • 6
  • 15
0

Update more information:

  • If you use flowLayout.estimatedItemSize, suggest use iOS8.3 later version. Before iOS8.3, it will crash [super layoutAttributesForElementsInRect:rect];. The error message is

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'

  • Second, in iOS8.x version, flowLayout.estimatedItemSize will cause different section inset setting did not work. i.e. function: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:.

Yang Young
  • 376
  • 2
  • 6
-2

I tried using estimatedItemSize but there were a bunch of bugs when inserting and deleting cells if the estimatedItemSize was not exactly equal to the cell's height. i stopped setting estimatedItemSize and implemented dynamic cell's by using a prototype cell. here's how that's done:

create this protocol:

protocol SizeableCollectionViewCell {
    func fittedSize(forConstrainedSize size: CGSize)->CGSize
}

implement this protocol in your custom UICollectionViewCell:

class YourCustomCollectionViewCell: UICollectionViewCell, SizeableCollectionViewCell {

    @IBOutlet private var mTitle: UILabel!
    @IBOutlet private var mDescription: UILabel!
    @IBOutlet private var mContentView: UIView!
    @IBOutlet private var mTitleTopConstraint: NSLayoutConstraint!
    @IBOutlet private var mDesciptionBottomConstraint: NSLayoutConstraint!

    func fittedSize(forConstrainedSize size: CGSize)->CGSize {

        let fittedSize: CGSize!

        //if height is greatest value, then it's dynamic, so it must be calculated
        if size.height == CGFLoat.greatestFiniteMagnitude {

            var height: CGFloat = 0

            /*now here's where you want to add all the heights up of your views.
              apple provides a method called sizeThatFits(size:), but it's not 
              implemented by default; except for some concrete subclasses such 
              as UILabel, UIButton, etc. search to see if the classes you use implement 
              it. here's how it would be used:
            */
            height += mTitle.sizeThatFits(size).height
            height += mDescription.sizeThatFits(size).height
            height += mCustomView.sizeThatFits(size).height    //you'll have to implement this in your custom view

            //anything that takes up height in the cell has to be included, including top/bottom margin constraints
            height += mTitleTopConstraint.constant
            height += mDescriptionBottomConstraint.constant

            fittedSize = CGSize(width: size.width, height: height)
        }
        //else width is greatest value, if not, you did something wrong
        else {
            //do the same thing that's done for height but with width, remember to include leading/trailing margins in calculations
        }

        return fittedSize
    }
}

now make your controller conform to UICollectionViewDelegateFlowLayout, and in it, have this field:

class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout {
    private var mCustomCellPrototype = UINib(nibName: <name of the nib file for your custom collectionviewcell>, bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell
}

it will be used as a prototype cell to bind data to and then determine how that data affected the dimension that you want to be dynamic

finally, the UICollectionViewDelegateFlowLayout's collectionView(:layout:sizeForItemAt:) has to be implemented:

class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

    private var mDataSource: [CustomModel]

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

        //bind the prototype cell with the data that corresponds to this index path
        mCustomCellPrototype.bind(model: mDataSource[indexPath.row])    //this is the same method you would use to reconfigure the cells that you dequeue in collectionView(:cellForItemAt:). i'm calling it bind

        //define the dimension you want constrained
        let width = UIScreen.main.bounds.size.width - 20    //the width you want your cells to be
        let height = CGFloat.greatestFiniteMagnitude    //height has the greatest finite magnitude, so in this code, that means it will be dynamic
        let constrainedSize = CGSize(width: width, height: height)

        //determine the size the cell will be given this data and return it
        return mCustomCellPrototype.fittedSize(forConstrainedSize: constrainedSize)
    }
}

and that's it. Returning the cell's size in collectionView(:layout:sizeForItemAt:) in this way preventing me from having to use estimatedItemSize, and inserting and deleting cells works perfectly.

shoe
  • 1,260
  • 1
  • 18
  • 39