Sunday, June 4, 2017

UICollectionView, full width cells, allow autolayout dynamic height?

Leave a Comment

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 height to be controlled by autolayout?

If so how?

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

6 Answers

Answers 1

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

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

Answers 3

There are couple of ways you could tackle this problem.

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

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

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

Answers 4

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.

Answers 5

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?

Answers 6

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: http://asciiwwdc.com/2016/sessions/219

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment