Discover ways to use UICollectionView, with extremely reusable UIKit elements and a few MVVM sample with out the going nuts with index path calculations.
Anatomy of the UICollectionView class
In the event you’re not conversant in UICollectionView, I’d recommend to get conversant in this class instantly. They’re the essential constructing blocks for a lot of apps offered by Apple and different third celebration builders. It’s like UITableView on steroids. Here’s a fast intro about how you can work with them by means of IB and Swift code. 💻
You may need observed that I’ve a love for steel music. On this tutorial we’re going to construct an Apple Music catalog like look from floor zero utilizing solely the mighty UICollectionView
class. Headers, horizontal and vertical scrolling, round photographs, so mainly virtually the whole lot that you simply’ll ever must construct nice person interfaces. 🤘🏻
Tips on how to make a UICollectionView utilizing Interface Builder (IB) in Xcode?
The quick & trustworthy reply: you shouldn’t use IB!
In the event you nonetheless wish to use IB, here’s a actual fast tutorial for completely learners:
The principle steps of making your first UICollectionView primarily based display are these:
- Drag a UICollectionView object to your view controller
- Set correct constraints on the gathering view
- Set dataSource & delegate of the gathering view
- Prototype your cell structure contained in the controller
- Add constraints to your views contained in the cell
- Set prototype cell class & reuse identifier
- Perform a little coding:
import UIKit
class MyCell: UICollectionViewCell {
@IBOutlet weak var textLabel: UILabel!
}
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
override func viewDidLayoutSubviews() {
tremendous.viewDidLayoutSubviews()
if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.itemSize = CGSize(
width: collectionView.bounds.width,
peak: 120
)
}
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(
in collectionView: UICollectionView
) -> Int {
1
}
func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection part: Int
) -> Int {
10
}
func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "MyCell",
for: indexPath
) as! MyCell
cell.textLabel.textual content = String(indexPath.row + 1)
return cell
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
print(indexPath.merchandise + 1)
}
}
In a nutshell, the information supply will present all of the required information about how you can populate the gathering view, and the delegate will deal with person occasions, reminiscent of tapping on a cell. It is best to have a transparent understanding concerning the information supply and delegate strategies, so be at liberty to play with them for a short while. ⌨️
Tips on how to setup a UICollectionView primarily based display programmatically?
As you may need observed cells are the core elements of a set view. They’re derived from reusable views, because of this in case you have an inventory of 1000 components, there received’t be a thousand cells created for each component, however just a few that fills the dimensions of the display and whenever you scroll down the listing this stuff are going to be reused to show your components. That is solely due to reminiscence issues, so in contrast to UIScrollView the UICollectionView (and UITableView) class is a very good and environment friendly one, however that is additionally the explanation why you need to put together (reset the contents of) the cell each time earlier than you show your precise information. 😉
Initialization can be dealt with by the system, nevertheless it’s value to say that in case you are working with Interface Builder, it is best to do your customization contained in the awakeFromNib
technique, however in case you are utilizing code, init(body:)
is your house.
import UIKit
class MyCell: UICollectionViewCell {
weak var textLabel: UILabel!
override init(body: CGRect) {
tremendous.init(body: body)
let textLabel = UILabel(body: .zero)
textLabel.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(textLabel)
NSLayoutConstraint.activate([
textLabel.topAnchor.constraint(
equalTo: contentView.topAnchor
),
textLabel.bottomAnchor.constraint(
equalTo: contentView.bottomAnchor
),
textLabel.leadingAnchor.constraint(
equalTo: contentView.leadingAnchor
),
textLabel.trailingAnchor.constraint(
equalTo: contentView.trailingAnchor
),
])
self.textLabel = textLabel
contentView.backgroundColor = .lightGray
textLabel.textAlignment = .middle
}
required init?(coder aDecoder: NSCoder) {
tremendous.init(coder: aDecoder)
fatalError("Interface Builder isn't supported!")
}
override func awakeFromNib() {
tremendous.awakeFromNib()
fatalError("Interface Builder isn't supported!")
}
override func prepareForReuse() {
tremendous.prepareForReuse()
textLabel.textual content = nil
}
}
Subsequent we’ve got to implement the view controller which is accountable for managing the gathering view, we’re not utilizing IB so we’ve got to create it manually by utilizing Auto Format anchors – like for the textLabel
within the cell – contained in the loadView
technique. After the view hierarchy is able to rock, we additionally set the information supply and delegate plus register our cell class for additional reuse. Be aware that that is accomplished robotically by the system in case you are utilizing IB, however if you happen to choose code you need to do it by calling the correct registration technique. You may register each nibs and lessons.
import UIKit
class ViewController: UIViewController {
weak var collectionView: UICollectionView!
override func loadView() {
tremendous.loadView()
let collectionView = UICollectionView(
body: .zero,
collectionViewLayout: UICollectionViewFlowLayout()
)
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(
equalTo: view.topAnchor
),
collectionView.bottomAnchor.constraint(
equalTo: view.bottomAnchor
),
collectionView.leadingAnchor.constraint(
equalTo: view.leadingAnchor
),
collectionView.trailingAnchor.constraint(
equalTo: view.trailingAnchor
),
])
self.collectionView = collectionView
}
override func viewDidLoad() {
tremendous.viewDidLoad()
collectionView.backgroundColor = .white
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(
MyCell.self,
forCellWithReuseIdentifier: "MyCell"
)
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(
in collectionView: UICollectionView
) -> Int {
1
}
func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection part: Int
) -> Int {
10
}
func collectionView(
_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "MyCell",
for: indexPath
) as! MyCell
cell.textLabel.textual content = String(indexPath.row + 1)
return cell
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
print(indexPath.row + 1)
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(
_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
.init(
width: collectionView.bounds.dimension.width - 16,
peak: 120
)
}
func collectionView(
_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt part: Int
) -> CGFloat {
8
}
func collectionView(
_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt part: Int
) -> CGFloat {
0
}
func collectionView(
_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
insetForSectionAt part: Int
) -> UIEdgeInsets {
.init(prime: 8, left: 8, backside: 8, proper: 8)
}
}
This time it is best to pay some consideration on the circulate structure delegate strategies. You should utilize these strategies to offer metrics for the structure system. The circulate structure will show all of the cells primarily based on these numbers and sizes. sizeForItemAt is accountable for the cell dimension, minimumInteritemSpacingForSectionAt
is the horizontal padding, minimumLineSpacingForSectionAt
is the vertical padding, and insetForSectionAt
is for the margin of the gathering view part.
So on this part I’m going to each use storyboards, nibs and a few Swift code. That is my traditional strategy for a couple of causes. Though I like making constraints from code, most individuals choose visible editors, so all of the cells are created inside nibs. Why nibs? As a result of in case you have a number of assortment views that is “virtually” the one good method to share cells between them.
You may create part footers precisely the identical method as you do headers, in order that’s why this time I’m solely going to deal with headers, as a result of actually you solely have to alter one phrase with a view to use footers. ⚽️
You simply need to create two xib recordsdata, one for the cell and one for the header. Please observe that you would use the very same assortment view cell to show content material within the part header, however this can be a demo so let’s simply go together with two distinct objects. You don’t even need to set the reuse identifier from IB, as a result of we’ve got to register our reusable views contained in the supply code, so simply set the cell class and join your shops.
Cell and supplementary component registration is barely completely different for nibs.
let cellNib = UINib(nibName: "Cell", bundle: nil)
self.collectionView.register(
cellNib,
forCellWithReuseIdentifier: "Cell"
)
let sectionNib = UINib(nibName: "Part", bundle: nil)
self.collectionView.register(
sectionNib,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: "Part"
)
Implementing the information supply for the part header seems to be like this.
func collectionView(
_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind type: String,
at indexPath: IndexPath
) -> UICollectionReusableView {
guard type == UICollectionView.elementKindSectionHeader else {
return UICollectionReusableView()
}
let view = collectionView.dequeueReusableSupplementaryView(
ofKind: type,
withReuseIdentifier: "Part",
for: indexPath
) as! Part
view.textLabel.textual content = String(indexPath.part + 1)
return view
}
Offering the dimensions for the circulate structure delegate can be fairly simple, nonetheless typically I don’t actually get the naming conventions by Apple. As soon as you need to swap a sort, and the opposite time there are actual strategies for particular sorts. 🤷♂️
func collectionView(
_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection part: Int
) -> CGSize {
.init(
width: collectionView.bounds.dimension.width,
peak: 64
)
}
Ranging from iOS9 part headers and footers will be pinned to the highest or backside of the seen bounds of the gathering view.
if let flowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.sectionHeadersPinToVisibleBounds = true
}
That’s it, now you know the way to construct primary layouts with assortment view.
What about complicated circumstances, like utilizing a number of sorts of cells in the identical assortment view? Issues can get fairly messy with index paths, in order that’s why I re-invented one thing higher primarily based on a way how you can construct superior person interfaces with assortment views showcased by Apple again at WWDC 2014.
My CollectionView primarily based UI framework
Now you recognize the fundamentals, so why don’t we get straight to the purpose? I’ll present you my finest observe of constructing nice person interfaces by utilizing my MVVM structure primarily based CollectionView micro framework.
CollectionView + ViewModel sample = ❤️ .
I’ll clarify the elements actual fast and after that you simply’ll discover ways to use them to construct up the Apple music-ish structure that I used to be speaking about to start with. 🎶
Grid system
The primary drawback with assortment views is the dimensions calculation. It’s a must to present the dimensions (width & peak) for every cell inside your assortment view.
- if the whole lot has a hard and fast dimension inside your assortment view, you possibly can simply set the dimensions properties on the circulate structure itself
- if you happen to want dynamic sizes per merchandise, you possibly can implement the circulate structure delegate aka. UICollectionViewDelegateFlowLayout (why is the delegate phrase in the midst of the identify???) and return the precise sizes for the structure system
- if you happen to want much more management you possibly can create a brand new structure subclass derived from CollectionView(Stream)Format and do all the dimensions calculations there
Thats good, however nonetheless you need to mess with index paths, trait collections, frames and lots of extra with a view to have a easy 2, 4, n column structure that adapts on each system. That is the explanation why I’ve created a very primary grid system for dimension calculation. With my grid class you possibly can simply set the variety of columns and get again the dimensions for x quantity of columns, “similar to” in internet primarily based css grid programs. 🕸
Cell reuse
Registering and reusing cells ought to and will be automated in a sort protected method. You simply wish to use the cell, and also you shouldn’t care about reuse identifiers and cell registration in any respect. I’ve made a pair helper strategies with a view to make the progress extra nice. Reuse identifiers are derived from the identify of the cell lessons, so that you dont’t have to fret about anymore. It is a observe that a lot of the builders use.
View mannequin
view mannequin = cell (view) + information (mannequin)
Filling up “template” cell with actual information must be the duty of a view mannequin. That is the place MVVM comes into play. I’ve made a generic base view mannequin class, that it is best to subclass. With the assistance of a protocol, you need to use varied cells in a single assortment view with out going loopy of the row & part calculations and you may deal with one easy activity: connecting view with fashions. 😛
Part
part = header + footer + cells
I’m making an attempt to emphasise that you simply don’t wish to mess with index paths, you simply wish to put your information collectively and that’s it. Previously I’ve struggled greater than sufficient with “pointless index path math”, so I’ve made the part object as a easy container to wrap headers, footers and all of the objects within the part. The end result? Generic information supply class that can be utilized with a number of cells with none row or part index calculations. 👏👏👏
Supply
So with a view to make all of the issues I’ve talked about above work, I wanted to implement the gathering view delegate, information supply, and circulate structure delegate strategies. That’s how my supply class was born. All the things is carried out right here, and I’m utilizing sections, view fashions the grid system to construct up assortment views. However hey, sufficient from this idea, let’s see it in observe. 👓
CollectionView framework instance utility
Tips on how to make a any listing or grid structure problem free? Effectively, as a primary step simply add my CollectionView framework as a dependency. Don’t fear you received’t remorse it, plus it helps Xcode 11 already, so you need to use the Swift Package deal Supervisor, straight from the file menu to combine this package deal.
Tip: simply add the @_exported import CollectionView
line within the AppDelegate file, then you definitely I don’t have to fret about importing the framework file-by-file.
Step 1. Make the cell.
This step is equivalent with the common setup, besides that your cell need to be a subclass of my Cell class. Add your personal cell and do the whole lot as you’ll do usually.
import UIKit
class AlbumCell: Cell {
@IBOutlet weak var textLabel: UILabel!
@IBOutlet weak var detailTextLabel: UILabel!
@IBOutlet weak var imageView: UIImageView!
override func awakeFromNib() {
tremendous.awakeFromNib()
self.textLabel.font = UIFont.systemFont(ofSize: 12, weight: .daring)
self.textLabel.textColor = .black
self.detailTextLabel.font = UIFont.systemFont(ofSize: 12, weight: .daring)
self.detailTextLabel.textColor = .darkGray
self.imageView.layer.cornerRadius = 8
self.imageView.layer.masksToBounds = true
}
override func reset() {
tremendous.reset()
self.textLabel.textual content = nil
self.detailTextLabel.textual content = nil
self.imageView.picture = nil
}
}
Step 2. Make a mannequin
Simply decide a mannequin object. It may be something, however my strategy is to make a brand new struct or class with a Mannequin suffix. This fashion I do know that fashions are referencing the gathering view fashions inside my reusable elements folder.
import Basis
struct AlbumModel {
let artist: String
let identify: String
let picture: String
}
Step 3. Make the view mannequin.
Now as a substitute of configuring the cell contained in the delegate, or in a configure technique someplace, let’s make an actual view mannequin for the cell & the information mannequin that’s going to be represented by way of the view.
import UIKit
class AlbumViewModel: ViewModel {
override func updateView() {
self.view?.textLabel.textual content = self.mannequin.artist
self.view?.detailTextLabel.textual content = self.mannequin.identify
self.view?.imageView.picture = UIImage(named: self.mannequin.picture)
}
override func dimension(grid: Grid) -> CGSize {
if
(self.collectionView.traitCollection.userInterfaceIdiom == .telephone &&
self.collectionView.traitCollection.verticalSizeClass == .compact) ||
self.collectionView?.traitCollection.userInterfaceIdiom == .pad
{
return grid.dimension(
for: self.collectionView,
ratio: 1.2,
objects: grid.columns / 4,
gaps: grid.columns - 1
)
}
if grid.columns == 1 {
return grid.dimension(for: self.collectionView, ratio: 1.1)
}
return grid.dimension(
for: self.collectionView,
ratio: 1.2,
objects: grid.columns / 2,
gaps: grid.columns - 1
)
}
}
Step 4. Setup your information supply.
Now, use your actual information and populate your assortment view utilizing the view fashions.
let grid = Grid(columns: 1, margin: UIEdgeInsets(all: 8))
self.collectionView.supply = .init(grid: grid, [
[
HeaderViewModel(.init(title: "Albums"))
AlbumViewModel(self.album)
],
])
self.collectionView.reloadData()
Step 5. 🍺🤘🏻🎸
Congratulations you’re accomplished along with your first assortment view. With just some traces of code you will have a ROCK SOLID code that can assist you out in a lot of the conditions! 😎
That is simply the tip of the iceberg! 🚢
What if we make a cell that accommodates a set view and we use the identical technique like above? A set view containing a set view… UICollectionViewception!!! 😂
It’s utterly attainable, and very easy to do, the information that feeds the view mannequin will probably be a set view supply object, and also you’re accomplished. Easy, magical and tremendous good to implement, additionally included within the instance app.
Sections with artists & round photographs
A number of sections? No drawback, round photographs? That’s additionally a bit of cake, if you happen to had learn my earlier tutorial about round assortment view cells, you’ll know how you can do it, however please try the supply code from GitLab and see it for your self in motion.
Callbacks and actions
Consumer occasions will be dealt with very straightforward, as a result of view fashions can have delegates or callback blocks, it solely relies on you which of them one you favor. The instance accommodates an onSelect handler, which is tremendous good and built-in to the framework. 😎
Dynamic cell sizing re-imagined
I additionally had a tutorial about assortment view self sizing cell assist, however to be trustworthy I’m not an enormous fan of Apple’s official technique. After I’ve made the grid system and began utilizing view fashions, it was simpler to calculate cell heights on my own, with about 2 traces of additional code. I consider that’s value it, as a result of self sizing cells are a bit of buggy if it involves auto rotation.
Rotation assist, adaptivity
Don’t fear about that an excessive amount of, you possibly can merely change the grid or examine trait collections contained in the view mannequin if you need. I’d say virtually the whole lot will be accomplished proper out of the field. My assortment view micro framework is only a light-weight wrapper across the official assortment view APIs. That’s the fantastic thing about it, be at liberty to do no matter you need and use it in a method that YOU personally choose. 📦
Now go, seize the pattern code and take heed to some steel! 🤘🏻
What if I instructed you… yet another factor: SwiftUI
These are some authentic quotes of mine again from April, 2018:
In the event you like this technique that’s cool, however what if I instructed you that there’s extra? Do you wish to use the identical sample all over the place? I imply on iOS, tvOS, macOS and even watchOS. Completed deal! I’ve created the whole lot contained in the CoreKit framework. UITableViews, WKInterfaceTables are supported as effectively.
Effectively, I’m a visionary, however SwiftUI was late 1 12 months, it arrived in 2019:
I actually consider that Apple this 12 months will strategy the subsequent technology UIKit / AppKit / UXKit frameworks (written in Swift after all) considerably like this. I’m not speaking concerning the view mannequin sample, however about the identical API on each platform considering. Anyway, who is aware of this for sue, we’ll see… #wwdc18 🤔
If somebody from Apple reads this, please clarify me why the hell is SwiftUI nonetheless an abstraction layer above UIKit/ AppKit as a substitute of a refactored AppleKit UI framework that lastly unifies each single API? For actual, why? Nonetheless don’t get it. 🤷♂️
Anyway, we’re moving into to the identical path guys, year-by-year I delete increasingly self-written “Third-party” code, so that you’re doing nice progress there! 🍎