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 format. Assortment view cells are customized views, however what if you would like to make use of the very same cell as a view?
Seems which you can present your personal UIContentConfiguration, identical to the built-in ones that you need to use to setup cells to seem like record objects. Should you check out the fashionable assortment views pattern code, which I extremely suggest, you may see the right way to implement customized content material configurations so as to create your personal cell varieties. There are some things that I do not like about this method. 😕
Initially, your view has to adapt to the UIContentView protocol, so you must deal with further config associated stuff contained in the view. I favor 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 handle the configuration updates. What if there was another approach?
Let’s begin our setup by creating a brand new subclass for our future cell object, we’re merely going to offer the standard initialize technique that I at all times use for my subclasses. Apple typically calls this technique configure of their samples, however they’re roughly 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 primary subclass so we do not have to cope with the init strategies anymore. Let’s create yet one more subclass primarily based on this object. The ReusableCell kind goes to be a generic kind, it may 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),
])
}
}
Through the use of this reusable cell kind, it may be attainable so as to add a customized view to the cell. We simply must create a brand new customized view, however that is fairly a simple activity 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 will pin to the superview with some further padding. You’ll be able to retailer all of your subviews as robust properties, since Apple goes to handle 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 peak, you need to merely pin the sting format constraints, however if you would like to make use of a set peak cell you’ll be able to add your personal peak anchor constraint with a continuing worth. It’s a must to set a customized precedence for the peak constraint this manner the auto format system will not break and it is going to have the ability to fulfill all the mandatory constraints.
Compositional format fundamentals
The UICollectionViewCompositionalLayout class is a extremely adaptive and versatile format device that you need to use to construct fashionable assortment view layouts. It has three important elements which you can configure to show your customized consumer interface parts in many alternative methods.
You mix the elements by build up from objects into a bunch, from teams into a piece, and at last right into a full format, like on this instance of a primary record format:
There are many nice sources and tutorials about this matter, so I will not get an excessive amount of into the small print now, however we will create a easy format that may show full width (fractional format dimension) objects in a full width group, by utilizing and estimated peak to assist 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 record format. 🙉
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 format = UICollectionViewCompositionalLayout(part: part)
return format
}
}
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 also create your personal auto format helper extensions, or use SnapKit to rapidly setup your format constraints. It’s comparatively simple to work with anchors, you need to learn my different tutorial about mastering auto format 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 price to say that just about all the things we speak about this tutorials is simply accessible on iOS14+ so in case you are planning to assist an older model you will not have the ability to use these options.
If you wish to be taught extra in regards to the matter, I might wish to suggest an article by Donny Wals and there’s a nice, however a bit longer submit by John Sundell about fashionable 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 will 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 will use the brand new UICollectionViewDiffableData class to specify our sections and objects inside the gathering view. You’ll be able to outline your sections as an enum, and on this case we will use a String kind as our objects. There’s a nice tutorial by AppCoda about diffable information sources.
Lengthy story brief, you need to 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 will apply an preliminary snapshot by appending a brand new part and our objects to the snapshot. You’ll be able to replace the information supply with the snapshot and the great factor about is it which you can additionally animate the modifications if you would like. 😍
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’ll be able to see it is fairly simple to provide you with a generic cell that can be utilized to render a customized view inside a set view. I imagine that the “official” cell configuration primarily based method is a little more sophisticated, plus you must write numerous code if it involves fashionable assortment views.
I will replace my unique assortment view framework with these new methods for positive. The brand new compositional format is far more highly effective in comparison with common movement layouts, diffable information sources are additionally superb and the brand new cell registration API can be good. I imagine that the gathering view crew at Apple did a tremendous job throughout the years, it is nonetheless one in every of my favourite elements if it involves UIKit growth. I extremely suggest studying these fashionable methods. 👍