UICollectionView
is an object that manages an ordered collection of data items and presents them using customizable layouts. (UICollectionview documentation )
UICollectionView
is a powerfull user interface building tool. User interface often requires to show UICollectionViewCells
wit content-based height.
As you can see, there are different solutions . After some research on the topic I stopped on the solution below, which looks stable and safe.
Case study
Create an applications to display Samuel L. Jacksons quotes. (We’ll take if from https://slipsum.com ). Each quote should be displayed in separate UICollectionViewCell
. Cell should be properly sized to fit the quote. UICollectionViewCell
and contained IULabel
should have bounding borders.
Another words, we will solve dynamic UICollectionViewCell
height problem for fixed width cell.
Solution
Create new XCode project (Use Swift language). Replace your AppDelegate.swift
code with the following:
AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate : UIResponder , UIApplicationDelegate {
var window : UIWindow ?
var navigationController : UINavigationController ?
func application ( _ application : UIApplication , didFinishLaunchingWithOptions launchOptions : [ UIApplication . LaunchOptionsKey : Any ]?) -> Bool {
window = UIWindow ( frame : UIScreen . main . bounds )
if let window = window {
let vc = ViewController ()
navigationController = UINavigationController ( rootViewController : vc )
window . rootViewController = navigationController
window . makeKeyAndVisible ()
}
return true
}
}
ViewController.swift
import UIKit
let samuelQuotes = [
"Normally, both your asses would be dead as fucking fried chicken, but you happen to pull this shit while I'm in a transitional period so I don't wanna kill you, I wanna help you. But I can't give you this case, it don't belong to me. Besides, I've already been through too much shit this morning over this case to hand it over to your dumb ass." ,
"Well, the way they make shows is, they make one show. That show's called a pilot." ,
"My money's in that office, right? If she start giving me some bullshit about it ain't there, and we got to go someplace else and get it, I'm gonna shoot you in the head then and there." ,
"The path of the righteous man is beset on all sides by the iniquities of the selfish and the tyranny of evil men." ,
"And I will strike down upon thee with great vengeance and furious anger those who would attempt to poison and destroy My brothers. And you will know My name is the Lord when I lay My vengeance upon thee."
]
class ViewController : UIViewController , UICollectionViewDataSource , UICollectionViewDelegateFlowLayout {
private(set) var collectionView : UICollectionView
// Initializers
init () {
// Create new `UICollectionView` and set `UICollectionViewFlowLayout` as its layout
collectionView = UICollectionView ( frame : . zero , collectionViewLayout : UICollectionViewFlowLayout ())
super . init ( nibName : nil , bundle : nil )
}
required init ?( coder aDecoder : NSCoder ) {
// Create new `UICollectionView` and set `UICollectionViewFlowLayout` as its layout
collectionView = UICollectionView ( frame : . zero , collectionViewLayout : UICollectionViewFlowLayout ())
super . init ( coder : aDecoder )
}
override func viewDidLoad () {
super . viewDidLoad ()
title = "Dynamic size sample"
// Register Cells
collectionView . register ( MultilineLabelCell . self , forCellWithReuseIdentifier : MultilineLabelCell . reuseId )
// Add `coolectionView` to display hierarchy and setup its appearance
view . addSubview ( collectionView )
collectionView . backgroundColor = . white
collectionView . contentInsetAdjustmentBehavior = . always
collectionView . contentInset = UIEdgeInsets ( top : 10 , left : 10 , bottom : 10 , right : 10 )
// Setup Autolayout constraints
collectionView . translatesAutoresizingMaskIntoConstraints = false
collectionView . bottomAnchor . constraint ( equalTo : view . bottomAnchor , constant : 0 ) . isActive = true
collectionView . leftAnchor . constraint ( equalTo : view . leftAnchor , constant : 0 ) . isActive = true
collectionView . topAnchor . constraint ( equalTo : view . topAnchor , constant : 0 ) . isActive = true
collectionView . rightAnchor . constraint ( equalTo : view . rightAnchor , constant : 0 ) . isActive = true
// Setup `dataSource` and `delegate`
collectionView . dataSource = self
collectionView . delegate = self
( collectionView . collectionViewLayout as! UICollectionViewFlowLayout ) . estimatedItemSize = UICollectionViewFlowLayout . automaticSize
( collectionView . collectionViewLayout as! UICollectionViewFlowLayout ) . sectionInsetReference = . fromLayoutMargins
}
// MARK: - UICollectionViewDataSource -
func collectionView ( _ collectionView : UICollectionView , cellForItemAt indexPath : IndexPath ) -> UICollectionViewCell {
let cell = collectionView . dequeueReusableCell ( withReuseIdentifier : MultilineLabelCell . reuseId , for : indexPath ) as! MultilineLabelCell
cell . configure ( text : samuelQuotes [ indexPath . row ])
return cell
}
func collectionView ( _ collectionView : UICollectionView , numberOfItemsInSection section : Int ) -> Int {
return samuelQuotes . count
}
// MARK: - UICollectionViewDelegateFlowLayout -
func collectionView ( _ collectionView : UICollectionView , layout collectionViewLayout : UICollectionViewLayout , sizeForItemAt indexPath : IndexPath ) -> CGSize {
let sectionInset = ( collectionViewLayout as! UICollectionViewFlowLayout ) . sectionInset
let referenceHeight : CGFloat = 100 // Approximate height of your cell
let referenceWidth = collectionView . safeAreaLayoutGuide . layoutFrame . width
- sectionInset . left
- sectionInset . right
- collectionView . contentInset . left
- collectionView . contentInset . right
return CGSize ( width : referenceWidth , height : referenceHeight )
}
}
MultilineLabelCell.swift
import UIKit
class MultilineLabelCell : UICollectionViewCell {
static let reuseId = "MultilineLabelCellReuseId"
private let label : UILabel = UILabel ( frame : . zero )
override init ( frame : CGRect ) {
super . init ( frame : frame )
layer . borderColor = UIColor . red . cgColor
layer . borderWidth = 1.0
label . numberOfLines = 0
label . lineBreakMode = . byWordWrapping
let labelInset = UIEdgeInsets ( top : 10 , left : 10 , bottom : - 10 , right : - 10 )
contentView . addSubview ( label )
label . translatesAutoresizingMaskIntoConstraints = false
label . topAnchor . constraint ( equalTo : contentView . layoutMarginsGuide . topAnchor , constant : labelInset . top ) . isActive = true
label . leadingAnchor . constraint ( equalTo : contentView . layoutMarginsGuide . leadingAnchor , constant : labelInset . left ) . isActive = true
label . trailingAnchor . constraint ( equalTo : contentView . layoutMarginsGuide . trailingAnchor , constant : labelInset . right ) . isActive = true
label . bottomAnchor . constraint ( equalTo : contentView . layoutMarginsGuide . bottomAnchor , constant : labelInset . bottom ) . isActive = true
label . layer . borderColor = UIColor . black . cgColor
label . layer . borderWidth = 1.0
}
required init ?( coder aDecoder : NSCoder ) {
fatalError ( "Storyboards are quicker, easier, more seductive. Not stronger then Code." )
}
func configure ( text : String ?) {
label . text = text
}
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
}
}
The result
Works both screen orientations:
Portrait
Landscape
Source code
Download from Github
Further reading