UICollectionView, full width cells, allow autolayo

2019-01-30 06:28发布

Here's a fairly basic question about current iOS development, to which I've never found a good answer.


In a (say) vertical UICollectionView ,

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

(If you're new to iOS "dynamic height", meaning the cell has a few, say, text views which could be any length or images which could be different sizes, so ultimately each cell is a totally different height.)

If so how?

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

13条回答
别忘想泡老子
2楼-- · 2019-01-30 06:48

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.

查看更多
太酷不给撩
3楼-- · 2019-01-30 06:49

There are couple of ways you could tackle this problem.

One way is you can give the collection view flow layout an estimated size and calculating the cell size.

Note: As mentioned in the comments below, as of iOS 10 you no longer need to provide and estimated size to trigger the call to a cells func preferredLayoutAttributesFitting(_ layoutAttributes:). Previously (iOS 9) would require you to provide an estimated size if you wanted to query a cells prefferedLayoutAttributes.

(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 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.

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 a instance of our cell from a storyboard
    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.

查看更多
Root(大扎)
4楼-- · 2019-01-30 06:49

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()
        }
    }
查看更多
Viruses.
5楼-- · 2019-01-30 06:53

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: https://janthielemann.de/ios-development/self-sizing-uicollectionviewcells-ios-10-swift-3/

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?

查看更多
相关推荐>>
6楼-- · 2019-01-30 06:56

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!

查看更多
疯言疯语
7楼-- · 2019-01-30 07:01

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

查看更多
登录 后发表回答