Movie Booking App - Part 2: Movie & Actor Detail View

MovieBookingApp Jan 09, 2020

Last week, I published the first part of this article… if you haven’t read it, here it is. In this second part, we will work on the movie and actor detail view. So, without further ado, let’s get started.

Here is what we are going to build:

You will need the finished first part to follow along, and you will get the finished project of this part HERE. If you are subscribed, please don’t subscribe again, check your email inbox… I sent the source code link to you. If you are not subscribed, you will need to do so to download the source code.

Single Movie View

In your Xcode root project, create a folder named Screens containing a swift UI file that you’ll name SingleMovieView.swift. Screens will contain our main views which in turn will be a combination of smaller parts that we will call views.

We want to use the same view for all our Movie types which are popular, trending and upcoming. This will be easily done because all our movies conform to the Movie protocol, so we will just need to make the SingleMovieView generic, and it should be able to be used by all the movie types.

Modify the definition of the SingleMovieView struct with the following:

struct SingleMovieView<T: Movie>: View {}

As you can see, our generic parameter must conform to the Movie protocol.

At the top of the struct, add this property var movie: T. This is the movie that we will work with.

Add this method below below the body block:

fileprivate func createPosterImage() -> some View {
           return Image(uiImage: UIImage(named: "\(movie.image).jpg") ?? UIImage() ).resizable()
           .aspectRatio(contentMode: .fit)
       }

This method just renders the poster image for the movie. You will have a compiler error in the preview. To fix it , replace the preview content with the following code:

    MovieDetailView<Popular>(movie: Popular.default)

And add the following code inside body:

ScrollView(showsIndicators: false) {
                    VStack(alignment: .leading) {
                        createPosterImage()
                    }
                }.edgesIgnoringSafeArea(.top)

This code is pretty self-explanatory. We make everything scrollable because we don’t know how tall the VStack will be. You will not be able to see what we have just done for now, but here is a preview:

Swift ui - Movie booking app
Swift ui - Movie booking app

Let’s now work on the remaining details. Create a Views folder, and inside it, add a swift ui file named MovieDetailView.swift. This will also be a generic view, so change its definition to this:

struct MovieDetailView<T: Movie>: View 

And add the following property to the top:

var movie: T

Then, below the body block, add the following method:

 fileprivate func createTitle() -> some View {
        return Text(self.movie.title)
        .font(.system(size: 35, weight: .black, design: .rounded))
        .layoutPriority(1)
        .multilineTextAlignment(.leading)
        .lineLimit(nil)
    }

The bit to note here is the layoutPriority. Setting it to 1 for the title text will make it write and fill the available space rather than be cut off. The default value is 0. Here is apple’s description of it:

Sets the priority by which a parent layout should apportion
space to this child.
The default priority is 0. In a group of sibling views,
raising a view's layout priority encourages that view to shrink
later when the group is shrunk and stretch sooner when the group
is stretched.
A parent layout should offer the child(ren) with the highest
layout priority all the space offered to the parent minus the
minimum space required for all its lower-priority children, and
so on for each lower priority value.

Then below that method, add this one:

 fileprivate func createGenreList() -> some View {
        return ScrollView(.horizontal) {
            HStack{
                ForEach(self.movie.genres, id: \.self){ genre in
                    Text(genre)
                        .bold()
                        .padding(5)
                        .background(Color.lightGray)
                        .cornerRadius(10)
                        .foregroundColor(Color.gray)
                    
                }
            }
        }
    }

This code just creates an horizontally scrolled list of genres styled properly.

Then add this below:

fileprivate func createDescription() -> some View {
        return Text(self.movie.description).lineLimit(nil).font(.body)
    }

Notice in the above code , we set the line limit to nil, doing so means there’s not linelimit. So the text is free to use as many lines as required.

And last add this last method below:

 fileprivate func createChooseSeatButton() -> some View {
        return LCButton(text: "Choose seats") {}
    }

The above code will get you an error because you haven’t created the LCButton. Open the finished project, and drag the LCButton.swift in your Views folder. The code is pretty simple to understand.

Before, we add everything in the body, we will need a view called LineRatingView that will render a line graph indicating a movie’s rating. So let’s create that.

In the Views folder, add the a swift ui file named LineRatingView.swift, and inside it, replace everything with this:

import SwiftUI

struct LineRatingView: View {
     var height: CGFloat = 10
    var value: Double = 0.0
    
    fileprivate func createPath(with size: CGSize) -> some View {
        return Rectangle()
            .frame(width: size.width, height: self.height, alignment: .leading).cornerRadius(self.height / 2)
            .foregroundColor(Color.gray.opacity(0.5))
    }
    
    fileprivate func createProgress(with size: CGSize) -> some View {
        return Rectangle()
            .fill(LinearGradient(gradient: Gradient(colors: [Color.accent, .darkPurple]) , startPoint: .leading, endPoint: .trailing))
            .animation(.easeIn(duration: 1))
            .frame(width: size.width * CGFloat(self.value) / 5, height: self.height, alignment: .leading)
            .cornerRadius(self.height / 2)
    }
    
    var body: some View {
        HStack {
            GeometryReader{ gr in
                ZStack(alignment: .leading) {
                    self.createPath(with: gr.size)
                    self.createProgress(with: gr.size)
                }
            }.frame(height: 15)
            
            Text("\(String(format: "%.1f", self.value))/5")
                .bold()
                .foregroundColor(Color.darkPurple)
        }
    }
}

struct LineRatingView_Previews: PreviewProvider {
    static var previews: some View {
        LineRatingView()
    }
}

The above code does the following:

  1. The first method create the path that has a gray background color and takes the entire width.
  2. The second method creates the progress indicator with a beautiful gradient color. This will be animatable reflecting the rating value.
  3. We then put everything inside a ZStack with the alignment set to leading for the progress to start on the left rather than the center.

Here is a preview of what we’ve just built:

Swift ui - Progress view
Swift ui - Progress view

Now, open the MovieDetailView.swift, and replace everything in the body block with the following:

 VStack(alignment: .leading) {
            createTitle()
            LineRatingView(value: movie.rating)
            createGenreList()
            
            HStack {
                 Text(self.movie.releaseDate).foregroundColor(Color.gray)
                Spacer()
                Text(self.movie.runtime).foregroundColor(Color.gray)
            }.padding(.vertical)
            
            createDescription()
            createChooseSeatButton()
            
}.padding(.horizontal).padding(.bottom, 20)

Go back to the SingleMovieView struct, and add the following below the createPosterImage() call inside the VStack container:


MovieDetailView(movie: self.movie)

Let’s now wire everything in the MovieStoreApp. Open the the MovieStoreApp.swift file, and put the following at the top:

    @State private var showDetails = false
    @State private var selectedIndex: Int?
    @State private var section: HomeSection = .Popular
    @State private var showSheet = false

Next, add these in the body block:

 var popular: [Popular] = []
        var trending: [Trending] = []
        var actors: [Actor]  = []
        var upcoming: [Upcoming] = []
        
        switch section {
        case .Popular:
            popular = model.allItems[section] as! [Popular]
        case .Actors:
            actors = model.allItems[section] as! [Actor]
        case .Trending:
            trending = model.allItems[section] as! [Trending]
        case .Upcoming:
            upcoming = model.allItems[section] as! [Upcoming]
        }

Then in the createCollectionView() method, inside didSelectItem closure, add the following:

 self.selectedIndex = indexPath.item
                self.section = HomeSection.allCases[indexPath.section]
                self.showSheet.toggle()

Next, in the body block, add the following modifier to the createCollectionView() method:

.sheet(isPresented: self.$showSheet) {
                if self.selectedIndex == nil{
                   Text("To be implemented")
                } else {
                    if self.section == HomeSection.Trending {
                        SingleMovieView(movie: trending[self.selectedIndex!])
                    }
                    if self.section == HomeSection.Popular {
                        SingleMovieView(movie: popular[self.selectedIndex!])
                        
                    }
                    if self.section == HomeSection.Upcoming {
                        SingleMovieView(movie: upcoming[self.selectedIndex!])
                    }
                    if self.section == HomeSection.Actors {
                    }
                }
            }

If the selectedIndex is nil, then the seeAll button was clicked which means we will show the vertically scrolled list of movies or actors, otherwise we show the singleMovieView passing in the respective type of movie.

Run the app and click any movie, you should see the SingleMovieView presented. Let’s now create the actor detail view now.

Actor Detail View

In the Screens folder, add a swift ui file named ActorDetailView.swift, and put the following code inside:

import SwiftUI

struct ActorDetailView: View {
    
    var actor: Actor
        
    var body: some View {
        ScrollView(showsIndicators: false) {
            VStack(alignment: .leading) {
                Image(uiImage: UIImage(named: actor.image) ?? UIImage() )
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                Text(actor.name)
                        .font(.system(size: 35, weight: .black, design: .rounded))
                        .padding(.horizontal)
                Text(actor.bio)
                    .font(.body)
                    .padding()
            }
        }.edgesIgnoringSafeArea(.top)
    }
}

struct ActorDetailView_Previews: PreviewProvider {
    static var previews: some View {
        ActorDetailView(actor:  Actor.default)
    }
}

Pretty simple, right? We’ve just stacked 3 views vertically in a scrollView.

Next, add the following code in the sheet content block where the section is equal HomeSection.Actors:

ActorDetailView(actor: actors[self.selectedIndex!])

With that in place, you should now be able to see the actor’s detail when you click any of the actors.

Movie List View

In the Screens folder, add a new swift ui file named MovieListView.swift containing the following code:

import SwiftUI

struct MovieListView<T: Movie>: View {
    
    var movies: [T]
    var section: HomeSection
    
    var body: some View {
        NavigationView {
            List(0..<movies.count) { i in
                    MovieListRow<T>(movie: self.movies[i])
            }.navigationBarTitle(section.rawValue)
        }
    }
}

struct MovieListView_Previews: PreviewProvider {
    static var previews: some View {
        MovieListView<Popular>(movies: [], section: .Trending)
    }
}

We also want the MovieListView to be generic in order to display all 3 types of movies. Your code will not compile because you are missing the MovieListRow, let’s create that now.

In the Views folder, create a swift ui file named MovieListRow.swift, and replace everything with the following :


struct MovieListRow<T: Movie>: View {
    var movie: T
    
    fileprivate func createImage() -> some View {
        return Image(uiImage: UIImage(named: "\(movie.image).jpg") ?? UIImage() )
        .resizable()
        .aspectRatio(contentMode: .fit).cornerRadius(20)
    }
        
    fileprivate func createTitle() -> some View {
        return Text(movie.title)
        .font(.system(size: 25, weight: .black, design: .rounded))
        .foregroundColor(Color.white)
    }
    
    var body: some View {
        
       return ZStack(alignment: .bottom) {
        createImage()
        
        VStack(alignment: .leading) {
                createTitle()
                LineRatingView(value: movie.rating)
            }.frame(maxWidth: .infinity, alignment: .leading)
                .padding()
                .background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.clear]) , startPoint: .bottom , endPoint: .top)).cornerRadius(20)
                .shadow(radius: 10)
        
        }.padding(.vertical)
    }
}


The above code is self-explanatory, and here is a preview:

Now in the MovieStoreApp, add the following code in the sheet condition where the selectedIndex is nil.

 if self.section == HomeSection.Trending {
                            MovieListView<Trending>(movies: trending, section: .Trending)
                        }
                        
                        if self.section == HomeSection.Popular {
                            MovieListView<Popular>(movies: popular, section: .Popular)
                            
                        }
                        if self.section == HomeSection.Upcoming {
                            MovieListView<Upcoming>(movies: upcoming, section: .Upcoming)
                        }
                        if self.section == HomeSection.Actors {
                            ActorListView(actors: actors, section: .Actors)
                        }

This code will run when one clicks on the seeAll button, and depending on the section, a list view of movies or actors will be presented. You will have a compiler error caused by the missing ActorListView, it’s almost similar to the MovieListView, so I won’t explain it again, but you can just copy the files from the finished project into yours. Before you run the app, add the following code in the seeAllForSection closure:

 self.section = section
 self.showSheet.toggle()
 self.selectedIndex = nil

And with that in place, you can now run the app and click the seeAll button to see the ListView in action.

Check out the next part (part 3) here.

Conclusion

After publishing the first part, I got an amazing response from you guys which showed me that you like this kind of tutorials, and I really do enjoy making them, so I will make more of them for as long as I can. Subscribe and share this article. Happy coding!!!

John K

I am a software developer and code enthusiast. Do you want to work with me, have a suggestion or a request? Feel free to contact me at [email protected] or https://twitter.com/liquidcoder