UICollectionView Self Sizing Cells with Auto Layout

前端 未结 16 1756
野性不改
野性不改 2020-11-22 05:58

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

相关标签:
16条回答
  • 2020-11-22 06:06

    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


    0 讨论(0)
  • 2020-11-22 06:09

    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.

    0 讨论(0)
  • 2020-11-22 06:11

    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:

    0 讨论(0)
  • 2020-11-22 06:11

    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
    }
    
    0 讨论(0)
  • 2020-11-22 06:12

    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
    
    0 讨论(0)
  • 2020-11-22 06:12

    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.

    0 讨论(0)
提交回复
热议问题