Anatomy of the UICollectionView class
In case you’re not conversant in UICollectionView, I might counsel to get conversant in this class instantly. They’re the fundamental constructing blocks for a lot of apps supplied by Apple and different third social gathering builders. It is like UITableView on steroids. Here’s a fast intro about the right way to work with them via IB and Swift code. 💻
You may need seen that I’ve a love for steel music. On this tutorial we will construct an Apple Music catalog like look from floor zero utilizing solely the mighty UICollectionView
class. Headers, horizontal and vertical scrolling, round pictures, so mainly virtually all the things that you’re going to ever must construct nice consumer interfaces. 🤘🏻
The way to make a UICollectionView utilizing Interface Builder (IB) in Xcode?
The brief & trustworthy reply: you should not use IB!
In case you nonetheless need to use IB, here’s a actual fast tutorial for completely learners:
The principle steps of making your first UICollectionView based mostly 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 format 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,
top: 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 the right way to populate the gathering view, and the delegate will deal with consumer occasions, similar to tapping on a cell. It’s best to have a transparent understanding in regards to the information supply and delegate strategies, so be happy to play with them for a short time. ⌨️
The way to setup a UICollectionView based mostly display programmatically?
As you may need seen cells are the core elements of a group view. They’re derived from reusable views, because of this you probably have a listing of 1000 components, there will not be a thousand cells created for each component, however just a few that fills the dimensions of the display and while you scroll down the record 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 extremely good and environment friendly one, however that is additionally the rationale why it’s a must to put together (reset the contents of) the cell each time earlier than you show your precise information. 😉
Initialization can also be dealt with by the system, nevertheless it’s price to say that if you’re working with Interface Builder, it is best to do your customization contained in the awakeFromNib
methodology, however if you’re utilizing code, init(body:)
is your home.
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 just isn't supported!")
}
override func awakeFromNib() {
tremendous.awakeFromNib()
fatalError("Interface Builder just isn't supported!")
}
override func prepareForReuse() {
tremendous.prepareForReuse()
textLabel.textual content = nil
}
}
Subsequent we now have to implement the view controller which is chargeable for managing the gathering view, we’re not utilizing IB so we now have to create it manually by utilizing Auto Format anchors – like for the textLabel
within the cell – contained in the loadView
methodology. After the view hierarchy is able to rock, we additionally set the information supply and delegate plus register our cell class for additional reuse. Notice that that is executed routinely by the system if you’re utilizing IB, however when you desire code it’s a must to do it by calling the correct registration methodology. You’ll be able to register each nibs and courses.
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,
format collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath
) -> CGSize {
.init(
width: collectionView.bounds.dimension.width - 16,
top: 120
)
}
func collectionView(
_ collectionView: UICollectionView,
format collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt part: Int
) -> CGFloat {
8
}
func collectionView(
_ collectionView: UICollectionView,
format collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt part: Int
) -> CGFloat {
0
}
func collectionView(
_ collectionView: UICollectionView,
format 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 stream format delegate strategies. You should use these strategies to supply metrics for the format system. The stream format will show all of the cells based mostly on these numbers and sizes. sizeForItemAt is chargeable 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.
Utilizing supplementary components (part headers and footers)
So on this part I’ll each use storyboards, nibs and a few Swift code. That is my normal strategy for just a few causes. Though I like making constraints from code, most individuals desire visible editors, so all of the cells are created inside nibs. Why nibs? As a result of you probably have a number of assortment views that is “virtually” the one good method to share cells between them.
You’ll be able to create part footers precisely the identical approach as you do headers, in order that’s why this time I am solely going to deal with headers, as a result of actually you solely have to vary one phrase to be able to use footers. ⚽️
You simply should create two xib information, one for the cell and one for the header. Please word that you possibly can use the very same assortment view cell to show content material within the part header, however it is a demo so let’s simply go along with two distinct objects. You do not even should set the reuse identifier from IB, as a result of we now have to register our reusable views contained in the supply code, so simply set the cell class and join your retailers.
Cell and supplementary component registration is barely totally 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 stream format delegate can also be fairly simple, nevertheless generally I do not actually get the naming conventions by Apple. As soon as it’s a must to change a form, and the opposite time there are actual strategies for particular varieties. 🤷♂️
func collectionView(
_ collectionView: UICollectionView,
format collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection part: Int
) -> CGSize {
.init(
width: collectionView.bounds.dimension.width,
top: 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 is it, now you understand how to construct fundamental layouts with assortment view.
What about advanced 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 based mostly on a way the right way to construct superior consumer interfaces with assortment views showcased by Apple again at WWDC 2014.
My CollectionView based mostly UI framework
Now you already know the fundamentals, so why do not we get straight to the purpose? I will present you my greatest observe of constructing nice consumer interfaces through the use of my MVVM structure based mostly CollectionView micro framework.
CollectionView + ViewModel sample = ❤️ .
I will clarify the elements actual fast and after that you’re going to learn to use them to construct up the Apple music-ish format that I used to be speaking about at first. 🎶
Grid system
The primary downside with assortment views is the dimensions calculation. It’s important to present the dimensions (width & top) for every cell inside your assortment view.
- if all the things has a hard and fast dimension inside your assortment view, you’ll be able to simply set the dimensions properties on the stream format itself
- when you want dynamic sizes per merchandise, you’ll be able to implement the stream format delegate aka. UICollectionViewDelegateFlowLayout (why is the delegate phrase in the course of the title???) and return the precise sizes for the format system
- when you want much more management you’ll be able to create a brand new format subclass derived from CollectionView(Movement)Format and do all the dimensions calculations there
Thats good, however nonetheless it’s a must to mess with index paths, trait collections, frames and lots of extra to be able to have a easy 2, 4, n column format that adapts on each system. That is the rationale why I’ve created a extremely fundamental grid system for dimension calculation. With my grid class you’ll be able to simply set the variety of columns and get again the dimensions for x quantity of columns, “similar to” in internet based mostly css grid programs. 🕸
Cell reuse
Registering and reusing cells ought to and will be automated in a sort secure method. You simply need to use the cell, and also you should not care about reuse identifiers and cell registration in any respect. I’ve made a pair helper strategies to be able to make the progress extra nice. Reuse identifiers are derived from the title of the cell courses, so that you dont’t have to fret about anymore. It is a observe that many of the builders use.
View mannequin
view mannequin = cell (view) + information (mannequin)
Filling up “template” cell with actual information ought to 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 should use varied cells in a single assortment view with out going loopy of the row & part calculations and you may deal with one easy job: connecting view with fashions. 😛
Part
part = header + footer + cells
I am attempting to emphasise that you do not need to mess with index paths, you simply need to put your information collectively and that is it. Up to now 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 consequence? Generic information supply class that can be utilized with a number of cells with none row or part index calculations. 👏👏👏
Supply
So to be able to make all of the issues I’ve talked about above work, I wanted to implement the gathering view delegate, information supply, and stream format delegate strategies. That is how my supply class was born. All the things is applied right here, and I am utilizing sections, view fashions the grid system to construct up assortment views. However hey, sufficient from this idea, let’s have a look at it in observe. 👓
CollectionView framework instance software
The way to make a any record or grid format problem free? Nicely, as a primary step simply add my CollectionView framework as a dependency. Don’t fret you will not remorse it, plus it helps Xcode 11 already, so you should 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 haven’t got to fret about importing the framework file-by-file.
Step 1. Make the cell.
This step is an identical with the common setup, besides that your cell should be a subclass of my Cell class. Add your individual cell and do all the things 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 choose 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 title: 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 methodology someplace, let’s make an actual view mannequin for the cell & the information mannequin that is going to be represented through the view.
import UIKit
class AlbumViewModel: ViewModel<AlbumCell, AlbumModel> {
override func updateView() {
self.view?.textLabel.textual content = self.mannequin.artist
self.view?.detailTextLabel.textual content = self.mannequin.title
self.view?.imageView.picture = UIImage(named: self.mannequin.picture)
}
override func dimension(grid: Grid) -> CGSize {
if
(self.collectionView.traitCollection.userInterfaceIdiom == .cellphone &&
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 are executed together with your first assortment view. With just some strains of code you have got a ROCK SOLID code that may assist you out in many of the conditions! 😎
That is simply the tip of the iceberg! 🚢
Horizontal scrolling inside vertical scrolling
What if we make a cell that accommodates a group view and we use the identical methodology like above? A set view containing a group view… UICollectionViewception!!! 😂
It is utterly potential, and very easy to do, the information that feeds the view mannequin will probably be a group view supply object, and also you’re executed. Easy, magical and tremendous good to implement, additionally included within the instance app.
Sections with artists & round pictures
A number of sections? No downside, round pictures? That is additionally a bit of cake, when you had learn my earlier tutorial about round assortment view cells, you will know the right way to do it, however please take a look at 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 like. 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 am not a giant fan of Apple’s official methodology. After I’ve made the grid system and began utilizing view fashions, it was less difficult to calculate cell heights on my own, with about 2 strains of additional code. I imagine that is price it, as a result of self sizing cells are somewhat buggy if it involves auto rotation.
Rotation assist, adaptivity
Don’t fret about that an excessive amount of, you’ll be able to merely change the grid or examine trait collections contained in the view mannequin if you would like. I might say virtually all the things will be executed proper out of the field. My assortment view micro framework is only a light-weight wrapper across the official assortment view APIs. That is the fantastic thing about it, be happy to do no matter you need and use it in a approach that YOU personally desire. 📦
Now go, seize the pattern code and hearken to some steel! 🤘🏻
What if I instructed you… another factor: SwiftUI
These are some unique quotes of mine again from April, 2018:
In case you like this methodology that is cool, however what if I instructed you that there’s extra? Do you need to use the identical sample all over the place? I imply on iOS, tvOS, macOS and even watchOS. Completed deal! I’ve created all the things contained in the CoreKit framework. UITableViews, WKInterfaceTables are supported as properly.
Nicely, I am a visionary, however SwiftUI was late 1 yr, it arrived in 2019:
I actually imagine that Apple this yr will strategy the subsequent era UIKit / AppKit / UXKit frameworks (written in Swift in fact) considerably like this. I am not speaking in regards to the view mannequin sample, however about the identical API on each platform pondering. 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 do not get it. 🤷♂️
Anyway, we’re stepping 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! 🍎