Tips on how to make self sizing cells in Swift each for desk & assortment views supporting orientation modifications and dynamic font sorts.
UITableView
So let’s begin with a normal single-view template for iOS. Title the mission, and go straight to the Essential.storyboard
file. Choose your view controller, delete it and create a brand new UITableViewController
scene.
Set the desk view controller scene as preliminary view controller and create a TableViewController.swift
file with the corresponding class.
import UIKit
class TableViewController: UITableViewController {
var dataSource: [String] = [
"Donec id elit non mi porta gravida at eget metus.",
"Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.",
"Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Vestibulum id ligula porta felis euismod semper. Nullam id dolor id nibh ultricies vehicula ut id elit. Nullam quis risus eget urna mollis ornare vel eu leo.",
"Maecenas faucibus mollis interdum.",
"Donec ullamcorper nulla non metus auctor fringilla. Aenean lacinia bibendum nulla sed consectetur. Cras mattis consectetur purus sit amet fermentum.",
"Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Maecenas faucibus mollis interdum.",
]
}
extension TableViewController {
override func tableView(
_ tableView: UITableView,
numberOfRowsInSection part: Int
) -> Int {
return dataSource.rely
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "Cell",
for: indexPath
) as! TableViewCell
cell.dynamicLabel?.textual content = dataSource[indexPath.row]
cell.dynamicLabel.font = UIFont.preferredFont(forTextStyle: .physique)
return cell
}
}
The setup is actually self-descriptive. You’ve acquired a string array as knowledge supply, and the required implementation of the UITableViewDataSource
protocol.
The one factor that’s lacking is the TableViewCell
class.
class TableViewCell: UITableViewCell {
@IBOutlet weak var dynamicLabel: UILabel!
}
First, create the category itself, then with interface builder choose the desk view controller scene and drag a label to the prototype cell. Set the category of the prototype cell to TableViewCell
. The reusable identifier could be merely "Cell"
. Join the dynamicLabel outlet to the view. Give the label high, backside, main, trailing constraints to the superview with the default worth of 8. Choose the label, set the font to physique type and the traces property to zero. That’s how easy it’s. đŸ˜‚
Now you might be nearly prepared. You simply must set the estimated row top on the desk view. Contained in the TableViewController class change the viewDidLoad
technique like this:
override func viewDidLoad() {
tremendous.viewDidLoad()
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
}
The estimatedRowHeight
property will inform the system that the desk view ought to strive to determine the top of every cell dynamically. You must also change the rowHeight property to automated dimension, should you don’t do then the system will use a static cell top – that one from interface builder that you would be able to set on the cell. Now construct & run. You have got an exquisite desk view with self sizing cells. You’ll be able to even rotate your gadget, it’s going to work in each orientations.
Yet one more factor
If you happen to change the textual content measurement beneath the iOS accessibility settings, the desk view will replicate the modifications, so it’ll adapt the format to the brand new worth. The font measurement of the desk view goes to alter in accordance with the slider worth. You would possibly wish to subscribe to the UIContentSizeCategory.didChangeNotification
so as to detect measurement modifications and reload the UI. This characteristic known as dynamic kind.
NotificationCenter.default.addObserver(
self.tableView,
selector: #selector(UITableView.reloadData),
identify: UIContentSizeCategory.didChangeNotification,
object: nil
)
UICollectionView
So we’ve completed the straightforward half. Now let’s attempt to obtain the similar performance with a group view. UICollectionView
is a generic class, that’s designed to create customized layouts, due to this generic habits you will be unable to create self sizing cells from interface builder. It’s a must to do it from code.
Earlier than we begin, we are able to nonetheless play with IB a little bit bit. Create a brand new assortment view controller scene, and drag a push segue from the earlier desk view cell to this new controller. Lastly embed the entire thing in a navigation controller.
The cell goes to be the very same as we used for the desk view, however it’s a subclass of UICollectionViewCell
, and we’re going to assemble the format instantly from code.
class CollectionViewCell: UICollectionViewCell {
weak var dynamicLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been applied")
}
override init(body: CGRect) {
tremendous.init(body: body)
translatesAutoresizingMaskIntoConstraints = false
let label = UILabel(body: bounds)
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.preferredFont(forTextStyle: .physique)
label.backgroundColor = UIColor.darkGray
label.numberOfLines = 0
label.preferredMaxLayoutWidth = body.measurement.width
self.contentView.addSubview(label)
self.dynamicLabel = label
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(
equalTo: dynamicLabel.topAnchor
),
contentView.bottomAnchor.constraint(
equalTo: dynamicLabel.bottomAnchor
),
contentView.leadingAnchor.constraint(
equalTo: dynamicLabel.leadingAnchor
),
contentView.trailingAnchor.constraint(
equalTo: dynamicLabel.trailingAnchor
),
])
}
override func prepareForReuse() {
tremendous.prepareForReuse()
dynamicLabel.font = UIFont.preferredFont(forTextStyle: .physique)
}
func setPreferred(width: CGFloat) {
dynamicLabel.preferredMaxLayoutWidth = width
}
}
We now have a subclass for our cell, now let’s create the view controller class. Contained in the viewDidLoad technique you must set the estimatedItemSize property on the gathering view. There should you give mistaken measurement, the auto-rotation gained’t work as anticipated.
override func viewDidLoad() {
tremendous.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .refresh,
goal: self,
motion: #selector(toggleColumns)
)
collectionView?.register(
CollectionViewCell.self,
forCellWithReuseIdentifier: "Cell"
)
if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.itemSize = CGSize(width: 64, top: 64)
flowLayout.minimumInteritemSpacing = 10
flowLayout.minimumLineSpacing = 20
flowLayout.sectionInset = UIEdgeInsets(
high: 10,
left: 10,
backside: 10,
proper: 10
)
flowLayout.estimatedItemSize = CGSize(
width: preferredWith(forSize: view.bounds.measurement),
top: 64
)
}
collectionView?.reloadData()
NotificationCenter.default.addObserver(
collectionView!,
selector: #selector(UICollectionView.reloadData),
identify: UIContentSizeCategory.didChangeNotification,
object: nil
)
}
Contained in the rotation strategies, you must invalidate the gathering view format, and recalculate the seen cell sizes when the transition occurs.
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
tremendous.traitCollectionDidChange(previousTraitCollection)
guard
let previousTraitCollection = previousTraitCollection,
traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass ||
traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass
else {
return
}
collectionView?.collectionViewLayout.invalidateLayout()
collectionView?.reloadData()
}
override func viewWillTransition(
to measurement: CGSize,
with coordinator: UIViewControllerTransitionCoordinator
) {
tremendous.viewWillTransition(to: measurement, with: coordinator)
collectionView?.collectionViewLayout.invalidateLayout()
estimateVisibleCellSizes(to: measurement)
coordinator.animate(alongsideTransition: { context in
}, completion: { context in
collectionView?.collectionViewLayout.invalidateLayout()
})
}
There are two helper strategies to calculate the popular width for the estimated merchandise measurement and to recalculate the seen cell sizes.
func preferredWith(forSize measurement: CGSize) -> CGFloat {
var columnFactor: CGFloat = 1.0
if twoColumns {
columnFactor = 2.0
}
return (measurement.width - 30) / columnFactor
}
func estimateVisibleCellSizes(to measurement: CGSize) {
guard let collectionView else {
return
}
if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(
width: preferredWith(forSize: measurement),
top: 64
)
}
collectionView.visibleCells.forEach { cell in
if let cell = cell as? CollectionViewCell {
cell.setPreferred(width: preferredWith(forSize: measurement))
}
}
}
You’ll be able to even have a number of columns should you do the suitable calculations.
There is just one factor that I couldn’t remedy, however that’s only a log message. If you happen to rotate again the gadget a few of the cells will not be going to be seen and the format engine will complain about that these cells can’t be snapshotted.
Snapshotting a view that has not been rendered ends in an empty snapshot. Guarantee your view has been rendered at the least as soon as earlier than snapshotting or snapshot after display updates.
If you can also make this message disappear someway OS_ACTIVITY_MODE=disable
, please don’t hesitate to submit a pull request for the tutorials repository on GitHub. đŸ˜‰