SwiftUI: Movie Booking App (Part 1)

UIViewRepresentable & UICollectionViewCompositionalLayout

Liquidcoder
15 min readJan 10, 2020

Introduction

This is part one of the movie booking app series. In this part, we will create the home screen which displays a collectionView of movies. The source code of this series will not be public on GitHub, instead if you like this series, and you want to get the source code, click here so that I can send it to you.

This will help me analyze how many people like this series so that I can do more. If you’ve already subscribed, don’t subscribe again. Every existing subscriber will get an email every time I publish a new tutorial, and the source code will be included in it.

Here is a movie booking app in SwiftUI

Preparations

To get started, you should have Xcode installed on macOS Mojave or Catalina. If you haven’t downloaded the starter and final project for this article, get it here. The folder contains 2 projects, a starter project which contains all of the images and data we will use to display movies and a finished one for this first part. You will need the starter project to follow along.

Models

In the Xcode root folder, create a Models folder and inside it add MovieBundle.swift file containing the following code:

import SwiftUIstruct MovieBundle: Codable, Hashable {
let trending: [Trending]
let popular: [Popular]
let actors: [Actor]
let upcoming: [Upcoming]
}
protocol Movie: Codable, Hashable {
var id: Int { get }
var title: String { get }
var releaseDate: String { get }
var description: String { get }
var image: String { get }
var rating: Double { get }
var genres: [String] { get }
var runtime: String { get }
var studio: String? { get }
}
// MARK: - Trending
struct Trending: Movie {
let id: Int
let title, releaseDate, description, image: String
let rating: Double
let genres: [String]
let runtime: String
var studio: String? = ""

static var `default`: Trending{
.init(id: 0, title: "", releaseDate: "", description: "", image: "", rating: 0, genres: [], runtime: "", studio: "")
}
}
// MARK: - Actor
struct Actor: Codable, Hashable {
let id: Int
let name, bio, image: String

static var `default`: Actor{
.init(id: 0, name: "", bio: "", image: "")
}
}// MARK: - Popular
struct Popular: Movie {
let id: Int
let title, releaseDate, description, image: String
let rating: Double
let genres: [String]
let runtime: String
var studio: String? = ""

static var `default`: Popular{
.init(id: 0, title: "", releaseDate: "", description: "", image: "", rating: 0, genres: [], runtime: "", studio: "")
}
}// MARK: - Upcoming
struct Upcoming: Movie {
let id: Int
let title, releaseDate, description, image: String
let rating: Double
let genres: [String]
let runtime: String
var studio: String? = ""

static var `default`: Upcoming{
.init(id: 0, title: "", releaseDate: "", description: "", image: "", rating: 0, genres: [], runtime: "", studio: "")
}
}

Couple of things to note here:

  1. The MovieBundle struct is the one that we will use to decode the data.
  2. We then create a `Movie` protocol that all our movies must conform to, you will see why in the second part of the series.

That’s our model. Let’s create the ViewModel then.

ViewModel

import SwiftUIenum HomeSection: String, CaseIterable {
case Trending
case Popular
case Upcoming
case Actors
}
class MovieViewModel: ObservableObject {

@Published var allItems: [HomeSection:[Codable]] = [:]

init() {
getAll()
}

private func getAll(){
if let path = Bundle.main.path(forResource: "data", ofType: "json") {

do {
let url = URL(fileURLWithPath: path)
let data = try Data(contentsOf: url, options: .mappedIfSafe)
let decoder = JSONDecoder()
let result = try decoder.decode(MovieBundle.self, from: data)
allItems = [HomeSection.Trending: result.trending,
HomeSection.Popular: result.popular,
HomeSection.Upcoming: result.upcoming,
HomeSection.Actors: result.actors]

} catch let e{
print(e)
}
}
}
}

Here is a brief rundown of the above code:

  1. We create the HomeSection enum which will be used as keys to retrieve specific sections from the model.
  2. Then in the ViewModel, we read the file containing the data, decode it and set the published property to notify interested views so that they can reload and reflect the current state of the model.

UIViewRepresentable

If you take a look a the video or just open the App Store’s game tab and try scrolling, you will notice that no matter how hard you scroll, you will only scroll one item at a time creating a peekaboo effect. To achieve that effect, we must use UIKit’s UICollectionView combined with the newly introduced UICollectionViewCompositionalLayout.

In your Xcode’s root folder, create a folder named UIKit. This is where we are going to keep all UIKit related files. In that folder, create a swift file named MovieCollectionView.swift.

import SwiftUIstruct MovieCollectionView: UIViewRepresentable {}

You will get a compiler error, but it’s okay we will fix that shortly. Notice how we conform to the UIViewRepresentable protocol, this might be familiar if you’ve read the article where I showed how to create an onboarding screen in SwiftUI. If you haven’t read it, here it is. It sort of does the same thing except that we want to use a UIView rather than a UIViewController in SwiftUI.

Add this code inside that struct:

func makeUIView(context: Context) -> UICollectionView {

return UICollectionView()
}
func updateUIView(_ collectionView: UICollectionView, context: Context) {

}

The first method is the place where we are going to initialize whatever view we want to use in SwiftUI, in this case, it’s a `UICollectionView`. Treat the `makeUIView` method as `init(frame: CGRect)` in a normal UIKit UIView.

The second view is where you would normally update the view that you want to use in SwiftUI. There are 2 other methods, the first one being `makeCoordinator()` which we will learn more of shortly, and the last one is called `dismantleUIView(uiView: Self.UIViewType, coordinator: Self.Coordinator)` which you would normally use when this view is about to be killed, and want to clean up resources to avoid memory leaks and so on…

Coordinator

The coordinator will be the class that will coordinate, as its name implies, the interaction between SwiftUI with whatever UIView we are using. This is the class that will handle all the data source and delegate conformance.

Add the following class inside the same struct, below `updateUIView`:

class Coordinator: NSObject,UICollectionViewDataSource, UICollectionViewDelegate{var parent: MovieCollectionView

init(_ parent: MovieCollectionView) {
self.parent = parent
}

}

As I mentioned earlier, we conform the coordinator to the UICollectionView datasource and delegate. We also keep a reference to the parent struct which is the `MovieCollectionView`… You will see why in a moment, but first, override the following methods to silence that compiler error you got.

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 0
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return UICollectionViewCell()
}

In the same class, add the following method:

func createCompositionalLayout() -> UICollectionViewLayout {
return UICollectionViewFlowLayout()
}

We return the standard FlowLayout for now, but we will change with the UICollectionViewCompositionalLayout later on.
Now add the following method in MovieCollectionView.swift above the coordinator class:

func makeCoordinator() -> MovieCollectionView.Coordinator {
Coordinator(self)
}

This is the fourth method from the UIViewRepresentable protocol. Its only job is to initialise a new Coordinator instance that we will use shortly.
After you’ve done that, add the following in the`makeUIView` method:

let collectionView = UICollectionView(frame: .zero, collectionViewLayout: context.coordinator.createCompositionalLayout())
collectionView.backgroundColor = .clear

collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.alwaysBounceVertical = true
collectionView.showsVerticalScrollIndicator = false
return collectionView

A few things to note in the above code:
1. We retrieve the coordinator from the context which in turn gives us access to everything in the coordinator class.
2. We set the datasource and delegate to the coordinator because the coordinator conforms to those 2 protocols.
Before we continue, we need to create UICollectionView cells for each section.

Trending Cell

In the UIKit folder, create a TrendingCell.swift file, and put the following inside:

import UIKitclass TrendingCell: UICollectionViewCell {
static let reuseId: String = "TrendingCell"
var trending: Trending?{
didSet{
if let trending = self.trending {
imageView.image = UIImage(named: "\(trending.image)_land.jpg")
titleLabel.text = trending.title
}
}
}

lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "adastra_land.jpg")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .clear
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 10
return imageView
}()

lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "Ad Astra"
label.textColor = UIColor(named: "textColor")
label.font = UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .bold))
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 2
label.textColor = .white
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupCell()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}



private func setupCell(){
let gradientView = UIView(frame: CGRect(x: 0, y: self.frame.height / 4 , width: self.frame.width, height: self.frame.height / 2) )
gradientView.layer.cornerRadius = 20
let gradient = CAGradientLayer()
gradient.frame = gradientView.frame
gradient.colors = [UIColor.clear, UIColor.black.cgColor]

gradientView.layer.insertSublayer(gradient, at: 0)
contentView.addSubview(self.imageView)
self.imageView.addSubview(gradientView)
contentView.addSubview(titleLabel)


NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
imageView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.9),
imageView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 1),

titleLabel.leadingAnchor.constraint(equalTo: imageView.leadingAnchor, constant: 10),
titleLabel.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
titleLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
titleLabel.heightAnchor.constraint(equalToConstant: 50)
])
}

}

If you have worked with UIKit before this code will be simple, but if you haven’t, I will briefly run through what I have done:

  1. I initialize and configure views lazily using closure to make the code a bit cleaner, but also for performance purposes. Read more about lazy variables on Paul Hudson’s Hacking with swift website
  2. In the setup cell method, I constrain those 2 views to the contentView after adding them to it.
  3. I also created a gradient view at the bottom of the cell which will lay behind the title.

Here is a preview:

Popular Cell

In the same folder, create a PopularCell.swift file and put the following inside:

import SwiftUIclass PopularCell: UICollectionViewCell {
static let reuseId: String = "PopularCell"
var popular: Popular?{
didSet{
if let movie = self.popular {
imageView.image = UIImage(named: "\(movie.image).jpg")
titleLabel.text = movie.title
}
}
}

lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "adastra.jpg")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .clear
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 10
return imageView
}()

lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "Ad Astra"
label.textColor = UIColor(named: "textColor")
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 2
label.textColor = .secondaryLabel
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupCell()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}


private func setupCell(){

self.addSubview(self.imageView)
self.addSubview(self.titleLabel)

NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.heightAnchor.constraint(equalTo: self.widthAnchor,multiplier: (3/2)),

titleLabel.topAnchor.constraint(equalTo: self.imageView.bottomAnchor),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
titleLabel.heightAnchor.constraint(equalToConstant: 50)

])
}

}

This code is almost similar to the one above, so refer to it if you have trouble understanding.

Actor Cell

In the same folder, create a PopularCell.swift file and put the following inside:

import UIKitclass ActorCell: UICollectionViewCell {
static let reuseId: String = "ActorCell"
var actor: Actor?{
didSet{
if let actor = self.actor {
imageView.image = UIImage(named: "\(actor.image).jpg")
titleLabel.text = actor.name
}
}
}

lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "adastra.jpg")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .clear
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 10
return imageView
}()

lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "Ad Astra"
label.textColor = .secondaryLabel
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 2
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupCell()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}


private func setupCell(){

self.addSubview(self.imageView)
self.addSubview(self.titleLabel)


NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.heightAnchor.constraint(equalTo: self.widthAnchor,multiplier: (3/2)),

titleLabel.topAnchor.constraint(equalTo: self.imageView.bottomAnchor),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
titleLabel.heightAnchor.constraint(equalToConstant: 50)

])
}
}

This code is also similar to the popular cell with the exception of the actor property at the top.

Both of them create the following layout:

Upcoming Cell

In the same folder, create a UpcomingCell.swift file and put this inside:

import UIKitclass UpcomingCell: UICollectionViewCell {
static let reuseId: String = "UpcomingCell"
var upcoming: Upcoming?{
didSet{
if let upcoming = self.upcoming {
imageView.image = UIImage(named: "\(upcoming.image)_land.jpg")
titleLabel.text = upcoming.title
releaseDateLabel.text = upcoming.releaseDate
}
}
}

lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "adastra_land.jpg")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = .clear
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 10
return imageView
}()

lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = "Ad Astra"
label.textColor = UIColor(named:"textColor")
label.font = UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .bold))
label.numberOfLines = 2
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

lazy var releaseDateLabel: UILabel = {
let label = UILabel()
label.text = "December 25, 2019"
label.textColor = UIColor.gray
label.font = UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .regular))
label.numberOfLines = 2
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupCell()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
}

private func setupCell(){
contentView.addSubview(self.imageView)
contentView.addSubview(self.titleLabel)
contentView.addSubview(self.releaseDateLabel)

NSLayoutConstraint.activate([
imageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.5),
imageView.heightAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.25),

titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
titleLabel.heightAnchor.constraint(equalToConstant: 30),
releaseDateLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10),
releaseDateLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
releaseDateLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
releaseDateLabel.heightAnchor.constraint(equalToConstant: 15)
])
}
}

The above are works similarly as the above ones, but the layout rendered here will slightly be different. Here it is:

Section Header View

This is the view we will build in UIKit. Add a swift file named HeaderView.swift, and inside it, add the following code:

import UIKitclass HeaderView: UICollectionReusableView {

static let reuseId = "HeaderView"
var onSeeAllClicked = {}

lazy var name: UILabel = {
let label = UILabel()
label.text = "Popular"
label.textColor = UIColor(named: "textColor")
label.numberOfLines = 2
label.font = UIFontMetrics.default.scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .bold))
return label
}()

lazy var seeAll: UIButton = {
let button = UIButton(type: .system)
button.setTitle("See all", for: .normal)
button.setTitleColor(UIColor(named: "darkPurple"), for: .normal)
button.addTarget(self, action: #selector(seeAllMovies), for: .touchUpInside)
button.backgroundColor = .clear
return button
}()
lazy var HStack:UIStackView = {
let stack = UIStackView(arrangedSubviews: [name,seeAll])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .horizontal
stack.distribution = .equalSpacing
return stack
}()


override init(frame: CGRect) {
super.init(frame: frame)

self.addSubview(HStack)

NSLayoutConstraint.activate([
HStack.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20),
HStack.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -20),
HStack.topAnchor.constraint(equalTo: self.topAnchor),
HStack.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
}

required init?(coder: NSCoder) {
fatalError("Not happening...")
}

@objc fileprivate func seeAllMovies(){
self.onSeeAllClicked()
}
}

This code is similar to what we have done except we have a closure that will run when the `seeAll` button is clicked. You will see how it will be used in the MovieCollectionView later.

Here is the header view we’ve just created:

Now that we have the cells out of the way, the start working on the compositional layout.

UICollectionViewCompositionalLayout

If you’ve worked with the UICollectionView before, then you are aware of UICollectionViewFlowLayout. With the flow layout, the items in the collection view flow from one row or column (depending on the scrolling direction) to the next, with each row comprising as many cells as will fit. However, there’s a new way of creating complex layouts with ease in UIKit using the UICollectionViewCompositionalLayout.

Before, if one wanted to create a layout like the App Store’s app or game screen layout, he would combine multiple UICollectionViews scrolled horizontally within a UITableView or a UICollectionView scrolled vertically.

However, that’s not the case anymore with the introduction of the amazing UICollectionViewCompositionalLayout.

In a nutshell, here is the UICollectionViewCompositionalLayout breakdown:

Let’s now start creating layouts. We will start the shared layout that will be used by the Popular and Actor section because they have identical layouts.

In the coordinator class, below the cellForItemAt method, add the following:

func createSharedSection() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.93), heightDimension: .fractionalWidth(0.75))
let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
layoutSection.boundarySupplementaryItems = [createSectionHeader()]
return layoutSection
}

Let me explain the above code line by line:

  1. The itemSize is calculated by taking half of the group’s width using ` .fractionalWidth(0.5)` while the height will be equal to the group’s full height. Then we create an NSCollectionLayoutItem giving it its size.
  2. The layoutGroupSize is calculated the same way, but uses the section’s dimensions, and notice how we use ` .fractionalWidth(0.75)` to set the height, this is because we want the height to be `3/4` of the group’s width. Then we create the layout group which will scroll horizontally by giving it the group size and an array of layout Item.
  3. We then create an NSCollectionLayoutSection passing in the layout group.
  4. We define the scrolling behavior by setting the layout section’s orthogonalScrollingBehavior to `.groupPagingCentered` which will create the app Store’s peekaboo effect. Here are all the scrolling behavior you can try and see how they work:

Here is the layout in action:

5. Last, we create the section header by setting the section’s boundarySupplementaryItems. You will get a compiler error caused by the missing createSectionHeader method. So add the following code to silence that error:

func createSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let layoutSectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(80))
let layoutSectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
return layoutSectionHeader
}

You can learn more about the compositional layout here.

To avoid being repetitive, I am not going to add the code for the remaining layout here, but it almost similar to what I’ve just explained above. Just get the final project HERE if you don’t have it yet, and copy the those 2 methods in you code.

So far, we have been preparing the components of our compositional layout, now is the time to create it. Find the `createCompositionalLayout` method, and replace the return statement with the following:

func createCompositionalLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout{[weak self] index, environment in
switch index{
case 0:
return self?.createTrendingSection()
case 1:
return self?.createSharedSection()
case 2:
return self?.createUpcomingSection()
default:
return self?.createSharedSection()
}
}
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 20
layout.configuration = config
return layout
}

Voila!! that’s our compositional layout. For each section index, we return the appropriate layout, and we set the spacing between section to 20. We are not ready to run the app yet because we still haven’t implemented the DataSource, so let’s tackle that now.

UICollectionViewDataSource

At the top of the viewController struct, add the following properties:

var allItems: [HomeSection:[Codable]]
var didSelectItem: ((_ indexPath: IndexPath)->()) = {_ in }
var seeAllforSection: ((_ section: HomeSection)->()) = {_ in }

Here is a quick explanation of what they do:

  1. The first one is the data that we parsed earlier.
  2. The second one is a closure that will run every time one of the cells is selected.
  3. The third one is obviously also a closure that will run when the see all button on the header view is tapped.

And in `makeUIView`, below the code line that creates the collectionView, add the following code:

collectionView.register(TrendingCell.self, forCellWithReuseIdentifier: TrendingCell.reuseId)
collectionView.register(PopularCell.self, forCellWithReuseIdentifier: PopularCell.reuseId)
collectionView.register(UpcomingCell.self, forCellWithReuseIdentifier: UpcomingCell.reuseId)
collectionView.register(ActorCell.self, forCellWithReuseIdentifier: ActorCell.reuseId)
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader , withReuseIdentifier: HeaderView.reuseId)

In the above code, we register all of the cells and the headerView.

In the coordinator struct, add the following method:

func numberOfSections(in collectionView: UICollectionView) -> Int {
return parent.allItems.count
}

The above code returns the number of section, which 4 in our case. Then replace the return statement in numberOfItemsInSection with the following:

return parent.allItems[HomeSection.allCases[section]]?.count ?? 0

In this method, we return the number of items (Movies or Actors) for each section. And last, before we try a first run, replace everything in cellForItemAt with the following:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

switch indexPath.section {
case 0:
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TrendingCell.reuseId, for: indexPath) as? TrendingCell{
cell.trending = parent.allItems[HomeSection.Trending]?[indexPath.item] as? Trending
return cell
}
case 1:
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PopularCell.reuseId, for: indexPath) as? PopularCell{
cell.popular = parent.allItems[HomeSection.Popular]?[indexPath.item] as? Popular
return cell
}

case 2:
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: UpcomingCell.reuseId, for: indexPath) as? UpcomingCell{
cell.upcoming = parent.allItems[HomeSection.Upcoming]?[indexPath.item] as? Upcoming
return cell
}
default:
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ActorCell.reuseId, for: indexPath) as? ActorCell{
cell.actor = parent.allItems[HomeSection.Actors]?[indexPath.item] as? Actor
return cell
}
}
return UICollectionViewCell()
}

For each section, we dequeue and return the corresponding cells. Now try running the app… Only the “Hello world” shows up, right? We haven’t added the MovieCollectionView in SwiftUI yet.

Open MovieStoreApp, and add this at the top of the file:

@ObservedObject private var model = MovieViewModel()

Then replace the hello world with this:

let movieCollectionView = createCollectionView().edgesIgnoringSafeArea(.all).navigationBarTitle("Movies")

return NavigationView {
movieCollectionView
}

To silence the error you get, put the following below the body block:

fileprivate func createCollectionView() -> MovieCollectionView {
return MovieCollectionView(allItems: model.allItems, didSelectItem: { indexPath in }, seeAllforSection: { section in })
}

If you run the app, it looks naked without the section headers. So add the following method anywhere inside the Coordinator class:

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.reuseId, for: indexPath) as? HeaderView else { return UICollectionReusableView() }
header.name.text = HomeSection.allCases[indexPath.section].rawValue
header.onSeeAllClicked = { [weak self] in
self?.parent.seeAllforSection(HomeSection.allCases[indexPath.section])
}
return header
default:
return UICollectionReusableView()
}
}

The above code creates the header for each section and wires the onSeeAllClicked closure from the HeaderView class to the parent’s closure. Let’s also wire the `didSelectItem`.

Add the following below `cellForItemAt`:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
parent.didSelectItem(indexPath)
}

Now, run the app again… everything should look as expected. And dark mode is supported. With this, we say goodbye to UIKit in this series.

In the second part, we will work on the detail screens for both movies and actors, so make sure you are subscribed here, follow me on medium and twitter if you haven’t done so already. Happy Coding.

Important: You can read the next part HERE

Here is the full app we will build in this series:

About Me

I am John, a Software engineer, and Blogger. I am the founder of liquidcoder.com where I write about SwiftUI, and soon about backend technologies starting with golang. Follow me on twitter Liquidcoder or shoot me an email at john@liquidcoder.com.

This is article is originally published on my website: liquidcoder.com

--

--