Movie Booking App
Tags: Mobile App Development, Swift, Swiftui, iOS App Development, iOS

Hey guys, swift UI 2.0 is out, if you didn’t know now you know. So I wanted to update the movie booking app with the new changes. The main component that will be replaced is the UICollectionView as there's now a swiftUI alternative. With this change, we will loose the peekaboo effect, but that's nothing compared to what we will get . Let's jump right into it.

Requirements

Xcode 12 or greater

MacOS Catalina or greater

Swift 5.0 or greater

iOS 14 or greater

Starter and final project

Get started

I want to try a new format of writing where I will show you the code and the file that contains it, the preview and last the explanation, just create the file if you don’t have it already.

To follow along, you need download the previous project source code (use part 5) , and when you finish, try comparing your work with the final project.

Initial cleanup

We will need to get rid of some unnecessary files that we don’t need anymore… So, delete the entire Images folder, then in the Views/Cells folder add the following swiftUI files:

Add the following in the Views folder:

Now let’s start filling them up, shall we?

Now Playing

NowPlayingView.swift

import SwiftUI

import KingfisherSwiftUI struct NowPlayingView: View {



var movie: MovieViewModel



var body: some View {

return

KFImage(source: .network(movie.backdropUrl))

.resizable()

.aspectRatio(contentMode: .fit)

.overlay(

Text(movie.title)

.font(.title2)

.bold()

.foregroundColor(Color.white)

.frame(maxWidth: .infinity, alignment: .leading)

.padding(.leading, 10)

.padding(.bottom, 10)

.background(

LinearGradient(gradient: Gradient(colors: [Color.black, Color.clear]), startPoint: .bottom, endPoint: .top)

), alignment: .bottom

).cornerRadius(20)

}

} struct NowPlayingView_Previews: PreviewProvider {



static var previews: some View {

NowPlayingView(movie: MovieViewModel.default)

}

}

Explanation

The NowPlayingView will just be an image with a text overlay aligned to the bottom. We've also added a gradient background to the text to make it readable on light background.

Preview

Popular & Top rated

The top rated and popular section will use the same view, I will call the file Popular.swift, but feel free to be creative.

Popular.swift

import SwiftUI

import KingfisherSwiftUI struct PopularView: View {



var movie: MovieViewModel



var body: some View {

VStack( alignment: .leading, spacing: 0) {



KFImage(source: .network(movie.posterUrl)).resizable()

.aspectRatio(contentMode: .fit)

.frame(height: 225, alignment: .center)

.cornerRadius(20)

.clipped()

.padding(.top, 5)





Text(movie.title)

.font(.headline)

.multilineTextAlignment(.leading)

.frame(maxWidth: .infinity,alignment: .leading)

.padding(.all, 10)

}.frame(width: 150)



}

} struct PopularView_Previews: PreviewProvider {

static var previews: some View {

PopularView(movie: MovieViewModel.default)

}

}

Explanation

As you can see, in this view we stack the image and the title vertically.

Preview

Upcoming

UpcomingView.swift

import SwiftUI

import KingfisherSwiftUI struct UpcomingView: View {



var movie: MovieViewModel



var body: some View {

HStack{

KFImage(source: .network(movie.backdropUrl)).resizable()

.aspectRatio(contentMode: .fit)

.cornerRadius(10.0)



VStack(alignment: .leading) {

Text(movie.title).font(.title).bold()

Text(movie.releaseDate)

.foregroundColor(.gray) }



Spacer()

}

}

} struct UpcomingView_Previews: PreviewProvider {

static var previews: some View {

UpcomingView(movie: MovieViewModel.default)

}

}

Explanation

As you can see, in this view we stack the image and the title horizontally.

Preview

Dynamic Container

This will be a generic container that we will use to layout different types of views, in our case we will use only the newly introduced LazyGrid and LazyHStack , but sky is the limit, one might extend it to include other containers.

DynamicContainer.swift

import SwiftUI



enum ContainerType {

case stack, grid

}





var type: ContainerType

var data: [Model]

var content: (Model) -> Content





private let gridLayout = [

GridItem(.flexible(minimum: 100), spacing: 10, alignment: .center),

GridItem(.flexible(minimum: 100), spacing: 10, alignment: .center)

]





init( data: [Model]?, type: ContainerType = .stack,

self.data = data ?? []

self.type = type

self.content = content

}



@ViewBuilder

var body: some View {



switch type {

case .stack:

LazyHStack(spacing: 20) {

ForEach(data) { value in

self.content(value)

}

}.padding(.horizontal, 20)



case .grid:

LazyHGrid(rows: gridLayout, spacing: 0) {

ForEach(data) { value in

self.content(value)

}

}

}

}

} struct DynamicContainer : View where Content: View, Model: Identifiable {enum ContainerType {case stack, gridvar type: ContainerTypevar data: [Model]var content: (Model) -> Contentprivate let gridLayout = [GridItem(.flexible(minimum: 100), spacing: 10, alignment: .center),GridItem(.flexible(minimum: 100), spacing: 10, alignment: .center)init( data: [Model]?, type: ContainerType = .stack, @ViewBuilder content: @escaping (Model) -> Content) {self.data = data ?? []self.type = typeself.content = contentvar body: some View {switch type {case .stack:LazyHStack(spacing: 20) {ForEach(data) { value inself.content(value)}.padding(.horizontal, 20)case .grid:LazyHGrid(rows: gridLayout, spacing: 0) {ForEach(data) { value inself.content(value)

Explanation

That’s a big chunk of code, let’s break it down. The struct is generic where the first generic parameter must conform to Identifiable protocol and the second one must also conform to the View protocol. We declare the ContainerType enum inside the DynamicContainer because it will only be used in DynamicContainer .

We then declare all the properties that we want to pass in via the init method. The first one is just the type of the container to use, the second one is an array of the Model type and the last one is the content ,which in this case, is a closure taking in a Model and spitting out a View .

We’ve also created the a private gridLayout property which is not ideal, but in this example we just need one layout, so it will not kill us to just declare it above.

In the body block, we switch on the type of container (because we can now use switch statement in here) and return the different type of container based on the enum cases. We are now able to annotate body with @ViewBuilder to return different types of Views inside a condition.

Section View

The section view will be used to layout each section.

SectionView.swift

import SwiftUI



var sectionType: HomeSection = .NowPlaying

var allItems: [HomeSection: [MovieViewModel]]







var data = [Int]()





var body: some View {





return VStack(spacing: 5) {

HStack {

Text(sectionType.rawValue)

.font(.title2)

.bold() struct SectionView: View {var sectionType: HomeSection = .NowPlayingvar allItems: [HomeSection: [MovieViewModel]] @Binding var seeSection: HomeSection? @Binding var selectedMovie: MovieViewModel?var data = [Int]()var body: some View {return VStack(spacing: 5) {HStack {Text(sectionType.rawValue).font(.title2).bold() Spacer() Button(action: {

seeSection = sectionType

}, label: {

Text("See all")

})

}.padding(.horizontal, 20)





ScrollView(.horizontal, showsIndicators: false) {



switch sectionType {

case .NowPlaying:

DynamicContainer(data: allItems[sectionType]) { value in

NowPlayingView(movie: value)

.frame(width: UIScreen.main.bounds.width - 40)

.onTapGesture {

self.selectedMovie = value

}

}.frame(height: 220)



case .Popular:

DynamicContainer(data: allItems[sectionType]) { value in

PopularView(movie: value)



.onTapGesture {

self.selectedMovie = value

}



}



case .Upcoming:

DynamicContainer(data: allItems[sectionType], type: .grid) { value in

UpcomingView(movie: value)

.padding(.horizontal, 20)

.frame(width: UIScreen.main.bounds.width)

.onTapGesture {

self.selectedMovie = value

}

}.padding(.top, 5) case .TopRated:

DynamicContainer(data: allItems[sectionType]) { value in

PopularView(movie: value)

.onTapGesture {

self.selectedMovie = value

}



}

}

}



}

}

} struct SectionView_Previews: PreviewProvider {



static var previews: some View {

SectionView(allItems: [:], seeSection: .constant(.NowPlaying), selectedMovie: .constant(MovieViewModel.default))

}

}

Explanation

The above code might seem like a lot, but it’s just a bunch of repetition in the switch statement with some minor differences.

The only thing to explain is the DynamicContainer call, we set the data from the allItems map then in the content closure, we create the view that will act as a cell.

Add the following declaration to the top:

@State private var selectedMovie: MovieViewModel?

Then add the following function below the createCollectionView

fileprivate func createSections() -> some View {



ScrollView{

VStack(spacing: 20) {

SectionView(sectionType: .NowPlaying, allItems: model.sectionMovies, seeSection: $section, selectedMovie: $selectedMovie)

SectionView(sectionType: .Popular, allItems: model.sectionMovies, seeSection: $section, selectedMovie: $selectedMovie)

SectionView(sectionType: .Upcoming, allItems: model.sectionMovies, seeSection: $section, selectedMovie: $selectedMovie)

SectionView(sectionType: .TopRated, allItems: model.sectionMovies, seeSection: $section, selectedMovie: $selectedMovie)

}.padding(.top, 20)

}.navigationTitle(Text("Movies"))



}

Now replace the entire else block with the following

createSections()

.sheet(isPresented: .constant(self.section != nil || selectedMovie != nil), onDismiss: {

selectedMovie = nil

section = nil

}) {

selectedMovie.map { SingleMovieView(movieId: $0.id) }

section.map { MovieListView(section: $0) }

}

Now delete these lines:

@State private var showDetails = false

@State private var selectedIndexPath: IndexPath?

@State private var showSheet = false

And the createCollectionView function. You should also delete the UIKit folder as well as the files inside it.

Conclusion