A fast intro to trendy assortment views utilizing compositional structure, diffable information supply and reusable view parts.
UIKit
Reusable views inside a generic cell
All of us like to create customized views for constructing numerous consumer interface parts, proper? We additionally love to make use of assortment views to show information utilizing a grid or an inventory structure. Assortment view cells are customized views, however what if you would like to make use of the very same cell as a view?
Seems that you may present your personal UIContentConfiguration, identical to the built-in ones that you need to use to setup cells to appear to be listing gadgets. For those who check out the trendy assortment views pattern code, which I extremely suggest, you will see easy methods to implement customized content material configurations with a view to create your personal cell sorts. There are some things that I do not like about this strategy. 😕
Initially, your view has to adapt to the UIContentView protocol, so you must deal with further config associated stuff contained in the view. I desire the MVVM sample, so this feels a bit unusual. The second factor that you just want is a customized cell subclass, the place you additionally need to care for the configuration updates. What if there was another means?
Let’s begin our setup by creating a brand new subclass for our future cell object, we’re merely going to supply the same old initialize
technique that I all the time use for my subclasses. Apple typically calls this technique configure
of their samples, however they’re kind of the identical. 😅
import UIKit
open class CollectionViewCell: UICollectionViewCell {
@accessible(*, unavailable)
non-public override init(body: CGRect) {
tremendous.init(body: body)
self.initialize()
}
@accessible(*, unavailable)
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder) isn not accessible")
}
open func initialize() {
}
}
All proper, that is only a fundamental subclass so we do not have to cope with the init strategies anymore. Let’s create yet another subclass based mostly on this object. The ReusableCell
sort goes to be a generic sort, it is going to have a view property, which goes to be added as a subview to the contentView
and we additionally pin the constraints to the content material view.
import UIKit
open class ReusableCell<View: UIView>: CollectionViewCell {
var view: View!
open override func initialize() {
tremendous.initialize()
let view = View()
view.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(view)
self.view = view
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: contentView.topAnchor),
view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
])
}
}
By utilizing this reusable cell sort, it is going to be attainable so as to add a customized view to the cell. We simply have to create a brand new customized view, however that is fairly a straightforward process to do. ✅
import UIKit
extension UIColor {
static var random: UIColor {
.init(crimson: .random(in: 0...1),
inexperienced: .random(in: 0...1),
blue: .random(in: 0...1),
alpha: 1)
}
}
class CustomView: View {
let label = UILabel(body: .zero)
override func initialize() {
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
addSubview(label)
backgroundColor = .random
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
label.topAnchor.constraint(equalTo: topAnchor, constant: 8),
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
])
}
}
This practice view has a label, which we are able to pin to the superview with some additional padding. You may retailer all of your subviews as sturdy properties, since Apple goes to care for the deinit, though the addSubview creates a robust reference, you do not have to fret about it anymore.
If you wish to create a cell that helps dynamic top, you must merely pin the sting structure constraints, however if you would like to make use of a set top cell you possibly can add your personal top anchor constraint with a relentless worth. You must set a customized precedence for the peak constraint this fashion the auto structure system will not break and it is going to have the ability to fulfill all the mandatory constraints.
Compositional structure fundamentals
The UICollectionViewCompositionalLayout class is a extremely adaptive and versatile structure software that you need to use to construct trendy assortment view layouts. It has three major parts that you may configure to show your customized consumer interface parts in many various methods.
You mix the parts by increase from gadgets into a gaggle, from teams into a bit, and at last right into a full structure,
like on this instance of a fundamental listing structure:
There are many nice assets and tutorials about this matter, so I will not get an excessive amount of into the small print now, however we’ll create a easy structure that may show full width (fractional structure dimension) gadgets in a full width group, through the use of and estimated top to help dynamic cell sizes. I suppose that is fairly a standard use-case for many people. We will create an extension on the UICollectionViewLayout
object to instantiate a brand new listing structure. 🙉
extension UICollectionViewLayout {
static func createListLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(44))
let merchandise = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let part = NSCollectionLayoutSection(group: group)
let structure = UICollectionViewCompositionalLayout(part: part)
return structure
}
}
Now it’s attainable so as to add a collectionView to our view hierarchy contained in the view controller.
class ViewController: UIViewController {
let collectionView = UICollectionView(body: .zero, collectionViewLayout: .createListLayout())
override func loadView() {
tremendous.loadView()
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: collectionView.topAnchor),
view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
])
}
override func viewDidLoad() {
tremendous.viewDidLoad()
}
}
You may as well create your personal auto structure helper extensions, or use SnapKit to shortly setup your structure constraints. It’s comparatively simple to work with anchors, you must learn my different tutorial about mastering auto structure anchors if you do not know a lot about them.
Cell registration and diffable information supply
Apple has a new set of APIs to register and dequeue cells for contemporary assortment views. It’s value to say that just about all the things we speak about this tutorials is simply accessible on iOS14+ so if you’re planning to help an older model you will not have the ability to use these options.
If you wish to study extra concerning the matter, I would prefer to suggest an article by Donny Wals and there’s a nice, however a bit longer submit by John Sundell about trendy assortment views. I am utilizing the identical helper extension to get a cell supplier utilizing a cell registration object, to make the method extra easy, plus we’ll want some random sentences, so let’s add a couple of helpers. 💡
extension String {
static func randomWord() -> String {
(0..<Int.random(in: 1...10)).map { _ in String(format: "%c", Int.random(in: 97..<123)) }.joined(separator: "")
}
static func randomSentence() -> String {
(0...50).map { _ in randomWord() }.joined(separator: " ")
}
}
extension UICollectionView.CellRegistration {
var cellProvider: (UICollectionView, IndexPath, Merchandise) -> Cell {
{ collectionView, indexPath, product in
collectionView.dequeueConfiguredReusableCell(utilizing: self, for: indexPath, merchandise: product)
}
}
}
Now we are able to use the brand new UICollectionViewDiffableData class to specify our sections and gadgets inside the gathering view. You may outline your sections as an enum, and on this case we’ll use a String sort as our gadgets. There’s a nice tutorial by AppCoda about diffable information sources.
Lengthy story brief, you must make a brand new cell configuration the place now you need to use the ReusableCell
with a CustomView
, then it’s attainable to setup the diffable information supply with the cellProvider on the cellRegistration object. Lastly we are able to apply an preliminary snapshot by appending a brand new part and our gadgets to the snapshot. You may replace the information supply with the snapshot and the good factor about is it that you may additionally animate the adjustments if you need. 😍
enum Part {
case `default`
}
class ViewController: UIViewController {
let collectionView = UICollectionView(body: .zero, collectionViewLayout: .createListLayout())
var dataSource: UICollectionViewDiffableDataSource<Part, String>!
let information: [String] = (0..<10).map { _ in String.randomSentence() }
override func loadView() {
tremendous.loadView()
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: collectionView.topAnchor),
view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
])
}
override func viewDidLoad() {
tremendous.viewDidLoad()
collectionView.delegate = self
createDataSource()
applyInitialSnapshot()
}
func createDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ReusableCell<CustomView>, String> { cell, indexPath, mannequin in
cell.view.label.textual content = mannequin
}
dataSource = UICollectionViewDiffableDataSource<Part, String>(collectionView: collectionView,
cellProvider: cellRegistration.cellProvider)
}
func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Part, String>()
snapshot.appendSections([.default])
snapshot.appendItems(information)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let merchandise = dataSource.itemIdentifier(for: indexPath)
print(merchandise ?? "n/a")
}
}
You continue to need to implement a delegate technique if you would like to deal with cell choice, however fortuitously the diffable information supply has an itemIdentifier technique to search for parts inside the information supply.
As you possibly can see it is fairly simple to give you a generic cell that can be utilized to render a customized view inside a set view. I imagine that the “official” cell configuration based mostly strategy is a little more difficult, plus you must write numerous code if it involves trendy assortment views.
I will replace my unique assortment view framework with these new strategies for certain. The brand new compositional structure is far more highly effective in comparison with common circulate layouts, diffable information sources are additionally wonderful and the brand new cell registration API can also be good. I imagine that the gathering view staff at Apple did an incredible job in the course of the years, it is nonetheless one among my favourite parts if it involves UIKit growth. I extremely suggest studying these trendy strategies. 👍