Learn to use UICollectionView, with extremely reusable UIKit elements and a few MVVM sample with out the going nuts with index path calculations.
UIKit
Anatomy of the UICollectionView class
In the event you’re not accustomed to UICollectionView, I would counsel to get accustomed to this class instantly. They’re the essential 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 tips on how to work with them by IB and Swift code. 💻
You may need observed 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 photos, so mainly virtually every thing that you will ever must construct nice person interfaces. 🤘🏻
Tips on how to make a UICollectionView utilizing Interface Builder (IB) in Xcode?
The quick & trustworthy reply: you should not use IB!
In the event you nonetheless wish to use IB, here’s a actual fast tutorial for completely newcomers:
The principle steps of making your first UICollectionView based mostly display screen 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 = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.itemSize = CGSize(width: self.collectionView.bounds.width, peak: 120)
}
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection part: Int) -> Int {
return 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 nuthsell, the info supply will present all of the required information about tips on how to populate the gathering view, and the delegate will deal with person occasions, comparable to tapping on a cell. You need to have a transparent understanding in regards to the information supply and delegate strategies, so be at liberty to play with them for a short time. ⌨️
Tips on how to setup a UICollectionView based mostly display screen programmatically in Swift 5?
As you may need observed cells are the core elements of a group view. They’re derived from reusable views, because of this in case you have a listing of 1000 components, there will not be a thousand cells created for each aspect, however just a few that fills the scale of the display screen and once you scroll down the checklist this stuff are going to be reused to show your components. That is solely due to reminiscence issues, so not like UIScrollView the UICollectionView (and UITableView) class is a extremely sensible and efficent one, however that is additionally the rationale why you need 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, but it surely’s price to say that if you’re working with Interface Builder, it is best to do your customization contained in the awakeFromNib
technique, 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
self.contentView.addSubview(textLabel)
NSLayoutConstraint.activate([
textLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor),
textLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
textLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
textLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
])
self.textLabel = textLabel
self.contentView.backgroundColor = .lightGray
self.textLabel.textAlignment = .heart
}
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()
self.textLabel.textual content = nil
}
}
Subsequent we have now to implement the view controller which is accountable for managing the gathering view, we’re not utilizing IB so we have now 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 info supply and delegate plus register our cell class for additional reuse. Word that that is executed robotically by the system if you’re utilizing IB, however in the event you want code you need to do it by calling the right registration technique. You possibly can 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
self.view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: self.view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.collectionView = collectionView
}
override func viewDidLoad() {
tremendous.viewDidLoad()
self.collectionView.backgroundColor = .white
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.collectionView.register(MyCell.self, forCellWithReuseIdentifier: "MyCell")
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection part: Int) -> Int {
return 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 {
return CGSize(width: collectionView.bounds.measurement.width - 16, peak: 120)
}
func collectionView(_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt part: Int) -> CGFloat {
return 8
}
func collectionView(_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt part: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
insetForSectionAt part: Int) -> UIEdgeInsets {
return UIEdgeInsets.init(high: 8, left: 8, backside: 8, proper: 8)
}
}
This time it is best to pay some consideration on the movement structure delegate strategies. You need to use these strategies to offer metrics for the structure system. The movement structure will show all of the cells based mostly on these numbers and sizes. sizeForItemAt is accountable for the cell measurement, 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 method for a couple of causes. Altought I really like making constraints from code, most individuals want visible editors, so all of the cells are created inside nibs. Why nibs? Becuase in case you have a number of assortment views that is “virtually” the one good strategy to share cells between them.
You possibly can 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 alter one phrase to be able to use footers. ⚽️
You simply need to create two xib information, one for the cell and one for the header. Please be aware that you may 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 together with two distinct objects. You do not even need to set the reuse identifier from IB, as a result of we have now to register our reusable views contained in the supply code, so simply set the cell class and join your shops.
Cell and supplementary aspect 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 info supply for the part header seems 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 scale for the movement structure delegate can also be fairly simple, nonetheless generally I do not actually get the naming conventions by Apple. As soon as you need to change a sort, and the opposite time there are precise strategies for particular sorts. 🤷♂️
func collectionView(_ collectionView: UICollectionView,
structure collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection part: Int) -> CGSize {
return CGSize(width: collectionView.bounds.measurement.width, peak: 64)
}
Ranging from iOS9 part headers and footers may 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 know the way 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 tips on how to construct superior person interfaces with assortment views showcased by Apple again at WWDC 2014.
My CollectionView based mostly UI framework
Now the fundamentals, so why do not we get straight to the purpose? I am going to present you my finest apply of constructing nice person interfaces through the use of my MVVM structure based mostly CollectionView micro framework. By the best way this framework is a good match if you’re planning to assemble enter types or advanced lists.
CollectionView + ViewModel sample = ❤️ .
I am going to clarify the elements actual fast and after that you will learn 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 downside with assortment views is the scale calculation. You must present the scale (width & peak) for every cell inside your assortment view.
- if every thing has a hard and fast measurement inside your assortment view, you may simply set the scale properties on the movement structure itself
- in the event you want dynamic sizes per merchandise, you may implement the movement structure delegate aka.
UICollectionViewDelegateFlowLayout
(why is the delegate phrase in the course of the title???) and return the precise sizes for the structure system - in the event you want much more management you may create a brand new structure subclass derived from CollectionView(Movement)Format and do all the scale calculations there
Thats good, however nonetheless you need to mess with index paths, trait collections, frames and plenty of extra to be able to have a easy 2, 4, n column structure that adapts on each system. That is the rationale why I’ve created a extremely fundamental grid system for measurement calculation. With my grid class you may simply set the variety of columns and get again the scale for x quantity of columns, “similar to” in internet based mostly css grid programs. 🕸
Cell reuse
Registering and reusing cells ought to and may be automated in a sort secure method. You simply wish 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. This can be a apply that a lot 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 utilize numerous 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 am attempting to emphasise that you do not wish to mess with index paths, you simply wish to put your information collectively and that is it. Prior 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 end result? 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 movement structure delegate strategies. That is how my supply class was born. The whole lot is carried out right here, and I am utilizing sections, view fashions the grid system to construct up assortment views. However hey, sufficient from this concept, let’s examine it in apply. 👓
CollectionView framework instance utility
Tips on how to make a any checklist or grid structure problem free? Effectively, as a primary step simply add my CollectionView framework as a dependency. Don’t be concerned you will not remorse it, plus it helps Xcode 11 already, so you should utilize the Swift Bundle 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 definately I haven’t got to fret about importing the framework file-by-file.
Step 1. Make the cell.
This step is similar with the common setup, besides that your cell need to be a subclass of my Cell
class. Add your personal cell and do every thing as you’d 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 method is to make a brand new struct or class with a Mannequin suffix. This manner 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 technique someplace, let’s make an actual view mannequin for the cell & the info 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 measurement(grid: Grid) -> CGSize {
if
(self.collectionView.traitCollection.userInterfaceIdiom == .cellphone &&
self.collectionView.traitCollection.verticalSizeClass == .compact) ||
self.collectionView?.traitCollection.userInterfaceIdiom == .pad
{
return grid.measurement(for: self.collectionView, ratio: 1.2, objects: grid.columns / 4, gaps: grid.columns - 1)
}
if grid.columns == 1 {
return grid.measurement(for: self.collectionView, ratio: 1.1)
}
return grid.measurement(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 along with your first assortment view. With only a few strains of code you might have a ROCK SOLID code that may provide help to out in a lot of the conditions! 😎
That is simply the tip of the iceberg! 🚢
Horizontal scrolling inside vertical scrolling
What if we make a cell that comprises a group view and we use the identical technique like above? A group view containing a collectionview… UICollectionViewception!!! 😂
It is utterly potential, and very easy to do, the info that feeds the view mannequin will likely 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 photos
A number of sections? No downside, round photos? That is additionally a bit of cake, in the event you had learn my earlier tutorial about cirular assortment view cells, you will know tips on how to do it, however please take a look at the supply code from gitlab and see it for youself in motion.
Callbacks and actions
Person occasions may be dealt with very straightforward, becuse view fashions can have delegates or callback blocks, it solely will depend on you which of them one you favor. The instance comprises an onSelect
handler, which is tremendous good and built-in to the framework. 😎
Dynamic cell sizing reimagined
I additionally had a tutorial about assortment view self sizing cell assist, however to be trustworthy I am 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 strains of additional code. I consider that is price it, as a result of self sizing cells are somewhat buggy if it involves autorotation.
Rotation assist, adaptivity
Don’t be concerned about that an excessive amount of, you may merely change the grid or verify trait collections contained in the view mannequin in order for you. I would say virtually every thing may 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 great thing about it, be at liberty to do no matter you need and use it in a approach that YOU personally want. 📦
Now go, seize the pattern code and take heed to some steel! 🤘🏻
What if I informed you… yet one more factor: SwiftUI
These are some unique quotes of mine again from April, 2018:
In the event you like this technique that is cool, however what if I informed you that there’s extra? Do you wish to use the identical sample in all places? I imply on iOS, tvOS, macOS and even watchOS. Carried out deal! I’ve created every thing contained in the CoreKit framework. UITableViews, WKInterfaceTables are supported as nicely.
Effectively, I am a visionary, however SwiftUI was late 1 yr, it arrived in 2019:
I actually consider that Apple this yr will method the following technology 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 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 do not get it. ¯_(ツ)_/¯
Anyway, we’re entering into to the identical route guys, year-by-year I delete increasingly more self-written / “Third-party” code, so that you’re doing nice progress there! 🍎