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条回答
Animai°情兽
2楼-- · 2019-01-30 06:38

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
  }
}
查看更多
劳资没心,怎么记你
3楼-- · 2019-01-30 06:39

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. why you don't take UITableVIew instead of UICollectionView in which you can just return in the heightForRow as UITableViewAutomaticDimension it will be managed automatically . anyways 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 ColllectionViewCell 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 ColllectionViewCell 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 .

查看更多
smile是对你的礼貌
4楼-- · 2019-01-30 06:40

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: https://stackoverflow.com/a/47424930/2171044, and provided a working sample app here.

查看更多
地球回转人心会变
5楼-- · 2019-01-30 06:40
  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: https://gist.github.com/madyanov/246217ad2628ba6a870114131a27c55c

Tested on iOS 11.

查看更多
beautiful°
6楼-- · 2019-01-30 06:45

You have to inherit the class UICollectionViewDelegateFlowLayout on your collectionViewController. Then add the function:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: view.frame.width, height: 100)
}

Using that, you have the width size of the screen width.

And you now have a collectionViewController with rows as a tableViewController.

If you want the size of the height of each cell to be dynamically, perhaps you should create custom cells for each cell you need.

查看更多
走好不送
7楼-- · 2019-01-30 06:47

With Swift 4.2 and iOS 12, you can subclass UICollectionViewFlowLayout and set its estimatedItemSize property to UICollectionViewFlowLayoutAutomaticSize (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 cells preferredLayoutAttributesFitting(_:) method and compute their compressed fitting 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.",
        "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. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
        "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."
    ]
    let columnLayout = FlowLayout()

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.alwaysBounceVertical = true
        collectionView.collectionViewLayout = columnLayout
        collectionView.contentInsetAdjustmentBehavior = .always
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
    }

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

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
        cell.label.text = items[indexPath.row]
        return cell
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    override init() {
        super.init()

        self.minimumInteritemSpacing = 10
        self.minimumLineSpacing = 10
        self.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else { return nil }
        guard let collectionView = collectionView else { return nil }
        layoutAttributes.bounds.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
        return layoutAttributes
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let superLayoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
        guard scrollDirection == .vertical else { return superLayoutAttributes }

        let computedAttributes = superLayoutAttributes.compactMap { layoutAttribute in
            return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute
        }
        return computedAttributes
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }

}

Cell.swift

import UIKit

class Cell: 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.layoutMarginsGuide.topAnchor).isActive = true
        label.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor).isActive = true
        label.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor).isActive = true
        label.bottomAnchor.constraint(equalTo: contentView.layoutMarginsGuide.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        layoutIfNeeded()
        let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
        layoutAttributes.bounds.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
        return layoutAttributes
    }

}

Here are some alternative implementations for preferredLayoutAttributesFitting(_:):

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutIfNeeded()
    label.preferredMaxLayoutWidth = label.bounds.size.width
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    label.preferredMaxLayoutWidth = layoutAttributes.size.width - contentView.layoutMargins.left - contentView.layoutMargins.left
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
    return layoutAttributes
}

Expected display:

enter image description here

查看更多
登录 后发表回答