Building Movie Trailer App Using SwiftUI

Developing a Movie Trailer App For Both iPhone and iPad Using SwiftUI

Shankar Madeshvaran
Dec 25, 2019 · 13 min read
Movie Trailer App — iPad and iOS

One of the latest framework that Apple has introduced in WWDC19 is SwiftUI and Combine. If in case missed it, SwiftUI is a new way of making UI in a declarative way. Combine works along with SwiftUI and provides a declarative Swift API for processing values such as UI or Network events.

For SwiftUI beginners, I recommend you to read this article which explains the basics of SwiftUI.

In this project, we are going to build a Movie Trailer App using SwiftUI concepts.

Movie Trailers App using SwiftUI

By building the Movie Trailer App, we are going to learn about:

  • How to load JSON data from a resource file
  • How to work with ForEach, ActionSheet, Image and Shapes like Rectangle
  • How to work with UI components like ScrollView , TabView, VStack, HStack, ZStack, NavigationLink, Button, etc
  • How to work with Property Observers like State, Binding, etc
  • How to work with UIKit’s ViewControllers in SwiftUI using UIViewRepresentable and UIViewControllerRepresentable
  • How to work with SFSafariViewController and UIPageViewController

1. Create a Model Class and JSON File

Movie.swift - Model Class

I have created a Model class contains details for each movie such as id, thumbnail, title, description, trailerLink, catagory and etc.,

Then I have created a Catagory which helps separate movies based on their genres:

Movies.json File

The Movies.json file contains a static json I created to develop a Movie App.

Decoding a JSON

Using JSONDecoder() I have decoded the JSON from the resource file movies.json. Now we can use moviesData on any Views which contains an array of movie details.

2. Dashboard

In this section, we are going to develop the dashboard of the Movie Trailer App. The dashboard will look like this:

Dashboard Screen

Designing For Movie Item

Designing For Each Movie Item

I have used VStack to align Image and description of the movie details such as Title and Description.

The image below represents the Xcode Preview for Particular Movie Item:

Xcode Preview For Single Movie Item

Designing Movie Item For a Row:

Designing Movie Item For Row

I have used VStack to align MovieItems and Movie Catagory title in verticle orientation. Then ScrollView to align each MovieItem in a horizontal order to form a row. And NavigationLink to navigate to MovieDetail page where it displays the movie details in detail.

Using ForEach we have aligned MovieItem in a horizontal orientation to achieve a row of movie items based on a number of elements in an array.

Xcode Preview for Each Row of Movie Items

Designing Movie Row Based on Movie Genres:

Displaying Movies List on Dashboard based on Genres

I created an array of movie details based on their Catagory by Using Grouping and filtering. Provided NavigationView to give Title for the dashboard and also do the navigation between views

By using ForEach and Keys of the catagories, I have created rows based on the genres count.

Now each MovieRow is created for each genre and by having an array of elements for each catagory, MovieItem is populated in each MovieRow:

ForEach(catagories.keys.sorted(), id: \String.self) { key in MovieRow(catagoryName: key,movies: self.catagories[key]!)        .frame(height: 320) 
.padding(.top)
.padding(.bottom)
}
Designing Movie Row Based On Genres

Interfacing with UIKit — Designing Featured Movies

SwiftUI works seamlessly with the existing UI frameworks on all Apple platforms. For example, you can place UIKit views and view controllers inside SwiftUI views, and vice versa.

This part of the tutorial shows you how to convert the featured movie list from the home screen to wrap instances of UIPageViewController and UIPageControl.

You’ll use UIPageViewController to display a carousel of SwiftUI views, and use state variables and bindings to coordinate data updates throughout the user interface.

1) Create View to Represent a UIPageViewController

To represent UIKit views and view controllers in SwiftUI, we need to create types that conform to the UIViewRepresentable and UIViewControllerRepresentable protocols.

Based on the developer’s custom types create and configure the UIKit types that they represent, while SwiftUI manages their life cycle and updates them when needed.

Step 1: Create a new SwiftUI view file, named PageViewController.swift, and declare the PageViewController type as conforming to UIViewControllerRepresentable.

The page view controller stores an array of UIViewController instances. These are the pages for scrolling between movies:

import SwiftUI
import UIKit

struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
}

Next, add the two requirements for the UIViewControllerRepresentable protocol.

Step 2: Add a makeUIViewController(context:) a method that creates a UIPageViewController with the desired configuration.

SwiftUI calls this method a single time when it’s ready to display the view, and then manages the view controller’s life cycle:

func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController( transitionStyle: .scroll, navigationOrientation: .horizontal)
return pageViewController
}

Step 3: Add an updateUIViewController(_:context:) method that calls setViewControllers(_:direction:animated:) to present the first view controller in the array for display.

func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[0]], direction: .forward, animated: true)
}

Step 4: Create a new SwiftUI view file, named PageView.swift, and update the PageView type to declare PageViewController as a child's view.

  • Notice that the generic initializer takes an array of views, and nests each one in a UIHostingController.
  • A UIHostingController is a UIViewController a subclass that represents a SwiftUI view within UIKit contexts.
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0)
}
}
var body: some View {
PageViewController(controllers: viewControllers)
}
}

Step 6: Pin the PageView preview to the canvas before you continue — this view is where all the action is.

struct PageView_Preview: PreviewProvider {
static var previews: some View {
PageView(featuredMovies.map { FeaturedMovieView(movie: $0) }) .frame(height: 225)
}
}

Step 7: To View the Preview, we need to create a design to show the Feature Movies. I have created a card like a structured view that contains Title, Thumbnail Image aligned in ZStack. Refer the below gist code:

FeaturedMovieView — Design to Show Featured Image

2) Create the View Controller’s Data Source

  • The PageViewController uses a UIPageViewController to show content SwiftUI from a SwiftUI view. Now it’s time to enable swiping interactions to move from page to page.
  • A SwiftUI view that represents a UIKit view controller can define a Coordinator type that SwiftUI manages and provides as part of the representable view’s context.

Step 1: Declare a nested Coordinator class inside PageViewController.

  • SwiftUI manages your UIViewControllerRepresentable the type’s coordinator and provides it as part of the context when calling the methods you created above.
class Coordinator: NSObject {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
}

Step 2: Add another method to PageViewController to make the coordinator.

  • SwiftUI calls this makeCoordinator() method before makeUIViewController(context:), so that you have access to the coordinator object when configuring your view controller.
  • You can use this coordinator to implement common Cocoa patterns, such as delegates, data sources, and responding to user events via target-action.
func makeCoordinator() -> Coordinator {
Coordinator(self)
}

Step 3: Add UIPageViewControllerDataSource conformance to the Coordinator type, and implement the two required methods.

  • These two methods establish the relationships between view controllers so that you can swipe back and forth between them.
Coordinator — Implemented Swipe Back and Forth

Step 4: Add the coordinator as a data source of the UIPageViewController

func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
return pageViewController
}

3) Track the Page in a SwiftUI View’s State

  • To prepare for adding a custom UIPageControl, you need a way to track the current page from within PageView.
  • To do this, you’ll declare a @State property in PageView, and pass a binding to this property down to the PageViewController view. The PageViewController updates the binding to match the visible page.

Step 1: Start by adding a currentPage binding as a property of PageViewController.

  • In addition to declaring the @Binding property, you also update the call to setViewControllers(_:direction:animated:), passing the value of the currentPage binding.
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@Binding var currentPage: Int
............ func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true)
................ }
}

Step 2: Declare the @State variable in PageView, and pass a binding to the property when creating the child PageViewController.

  • Use the $ syntax to create a binding to a value that is stored as a state.
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@State var currentPage = 0

init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}

var body: some View {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
}
}

Step 3: Add a text view with the currentPage property, so that you can keep an eye on the @State property’s value.

Observe that when you swipe from page to page, the value doesn’t change.

struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@State var currentPage = 0
................

var body: some View {
VStack {
PageViewController(controllers: viewControllers,currentPage: $currentPage)
Text("Current Page: \(currentPage)")
}
}
}

Step 4: In PageViewController.swift, conform the coordinator to UIPageViewControllerDelegate, and add the pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted completed: Bool) method.

  • Because SwiftUI calls this method whenever a page switching animation completes, you can find the index of the current view controller and update the binding.
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
................. func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of:visibleViewController)
{
parent.currentPage = index
}
}
}

Step 5: Assign the coordinator as the delegate for the UIPageViewController, in addition to the data source

  • With the binding connected in both directions, the text view updates to show the correct page number after each swipe.
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}

Check out the Below gist to view Full code for PageViewController:

PageViewController.swift

4) Add a Custom Page Control

You’re ready to add a custom UIPageControl to your view, wrapped in SwiftUI UIViewRepresentable view.

Step 1: Create a new SwiftUI view file, named PageControl.swift. Update the PageControl type to conform to the UIViewRepresentable protocol.

  • UIViewRepresentable and UIViewControllerRepresentable types have the same life cycle, with methods that correspond to their underlying UIKit types.
struct PageControl: UIViewRepresentable {
var numberOfPages: Int
@Binding var currentPage: Int

func makeUIView(context: Context) -> UIPageControl {
let control = UIPageControl()
control.numberOfPages = numberOfPages
return control
}

func updateUIView(_ uiView: UIPageControl, context: Context) {
uiView.currentPage = currentPage
}
}

Step 2: Replace the text box with the page control, switching from a VStack to a ZStack for layout.

  • Because you’re passing the page count and the binding to the current page, the page control is already showing the correct values.
PageView
  • Next, make the page control interactive so users can tap one side or the other to move between pages.

Step 3: Create a nested Coordinator type in PageControl, and add a makeCoordinator() method to create and return a new coordinator.

  • Because UIControl subclasses like UIPageControl use the target-action pattern instead of delegation, this Coordinator implements an @objc method to update the current page binding.
struct PageControl: UIViewRepresentable {
var numberOfPages: Int
@Binding var currentPage: Int

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

func makeUIView(context: Context) -> UIPageControl {
let control = UIPageControl()
control.numberOfPages = numberOfPages
return control
}

func updateUIView(_ uiView: UIPageControl, context: Context) {
uiView.currentPage = currentPage
}

class Coordinator: NSObject {
var control: PageControl
init(_ control: PageControl) {
self.control = control
}
@objc func updateCurrentPage(sender: UIPageControl) {
control.currentPage = sender.currentPage
}
}
}

Step 4: Add the coordinator as the target for the valueChanged event, specifying the updateCurrentPage(sender:) method as the action to perform.

struct PageControl: UIViewRepresentable {

..............
func makeUIView(context: Context) -> UIPageControl {
let control = UIPageControl()
control.numberOfPages = numberOfPages
control.addTarget(context.coordinator, action: #selector(Coordinator.updateCurrentPage(sender:)), for: .valueChanged)
return control
}
}

Step 5: Now try out all the different interactions — PageView shows how UIKit and SwiftUI views and controllers can work together and Checkout the below gist file see the full code for PageControl:

PageControl.swift-PageControl Integration with UIKit and SwiftUI

Integrate Featured Movies into Dashboard:

Now we need to add the FeaturedMovie List into the dashboard. For that, we have filtered featured movies from movies data:

var featuredMovies = moviesData.filter({$0.isFeaturedMovie == true})

Then we map each featured movie into Pageview which contains Page control to scroll through each movie:

PageView(featuredMovies.map { FeaturedMovieView(movie: $0) })

Refer the below gist code to get FeatureMovies List into the top of the dashboard:

FeaturedMoviesList — To Show in Dashboard
FeaturedMovieList — Xcode Preview
FeaturedMovies List With Page Control

We add the Movie Row code which shows movies based on the genres below featured movie:

HomeView.Swift — Complete Dashboard View
DashboardView — Xcode Preview

Add TabView to Dashboard:

A view that switches between multiple child views using interactive user interface elements.

To create a user interface with tabs, place views in a TabView and apply the tabItem(_:) modifier to the contents of each tab. The following creates a tab view with four tabs:

DashboardView-Added TabView to HomeView
  • Each Tab Item has customized with different Image and Text Views
  • Image are used by browsing through the SF Symbols App which Apple introduced during WWDC 2019. Apple basically gave us free symbols to use in our app and it’s even really easy to use them as well
  • The supported platforms for SF Symbols are iOS 13+, watchOS 6+ and tvOS 13+
  • Apple provides an SF Symbols app that allows you to browse, copy, and export any available symbols. The app can be downloaded here and is available for macOS 10.14 and later.

Navigating to the Movie Detail View

First, we need to embed List into NavigationView and I have already done while designing HomeView:

NavigationView {         
........
....... .navigationBarTitle(Text("MOVIES").bold()) }
.....

This is like embedding a view controller in a navigation controller: You can now access all the navigation things, like the navigation bar title. Notice .navigationBarTitle modifies List, not NavigationView. You can declare more than one view in a NavigationView, and each can have its own .navigationBarTitle.

We will get a large title MOVIES by default.

Creating a Navigation Link

NavigationView also enables NavigationLink, which needs a destination view and a label.

Inside the List closure in HomeView, make the row view Text() into a NavigationLink button:

NavigationLink(destination: MovieDetail(movie: featuredMovies[0])) {                                               PageView(featuredMovies.map { FeaturedMovieView(movie: $0) })                                                   .frame(height: 225)                                           
}
Navigation From FeaturedMovies List
ForEach(self.movies,id: \.title) { movie in                                              NavigationLink(destination: MovieDetail(movie: movie)) {                                                        MovieItem(movie: movie)    
...... }
}
Navigation For Movie Item

3. Movie Detail Screen

In this section, we are going to develop the Movie Detail Screen. Movie Detail Screen will look like this:

Movie Detail Screen

Designing Trailer Button:

Watch Trailer Button

When creating a button, you need to provide two code blocks:

  1. What to perform — the code to perform after the button is tapped or selected by the user.
  2. How the button looks like — the code block that describes the look & feel of the button.

In the above gists I have customized buttons based on my preference, I have also used State Property observer to change showingDetail variable value when a button is tapped.

In SwiftUI, when property observe value is changed it will reload the views where that variable is used. Hence, it will show the TrailerView which I will explain in the below section.

I have used properties like frame, foregroundColor, background and cornerRadius to customize button design based on my preference.

Watch Trailer Button — Xcode Preview

Designing Movie Detail Screen

In this screen, I have used navigationBarHidden(true) to hide the navigation bar and also used edgesIgnoringSafeArea(.top) to ignore the safe area to the Top of the screen.

Using ZStack , I have shown the Image and Title of the movie in top of the MovieDetail page. Using VStack, I have shown the Description of the movie below the Title.

Integrated WatchButton View below the description of the movie. Refer the below gists to see the code for MovieDetail Page:

MovieDetail.swift
MovieDetail — Xcode Preview

4. Trailer Screen

In this section, we are going to develop the Trailer Screen. Trailer Screen will look like this:

Tailer Screen

Integrating SFSafariViewController in SwiftUI

Using UIViewControllerRepresentable, I have integrated SafariViewController into SwiftUI.

We need to add a makeUIViewController(context:) a method that creates a SFSafariViewController with the desired configuration.SwiftUI calls this method a single time when it’s ready to display the view, and then manages the view controller’s life cycle.

Add an updateUIViewController(_:context:) a method that calls to dismiss the view controller when the dismiss button is Tapped.

SafariView-Integrating SFSafariViewController into SwiftUI

Now we create a new SwiftUI view TrailerView which is called when WatchTrailer Button is tapped from Movie Detail page.

After that call the SafariView with the URL of the trailer link and now link will load from the SafariViewController and trailer will be played from youtube.

Check out the below code to call the SFSafariViewController with the URL of the Trailer:

TrailerView — Load Trailer From URL
TrailerView — Xcode Preview

5) Resources:

Find the detailed ScreenShots and project code in this Github link. You can refer to it in case you have any queries.

The Project is Updated for Xcode 11+ and Swift 5.0

6) Conclusion

I hope you have learned how to build an app using SwiftUI, work with different UI Components in SwiftUI and how to integrate UIKit into SwiftUI.

I’ll think of expanding this app to show tailers for TV Shows and many more shows and movies to try out many concepts in SwiftUI.

If you liked this app and want to build more, then star and follow this repository to get updates whenever I have done some changes or added new modules for Movie Trailer App.

Materials and articles which I referred to develop this Movie Trailer App also mentioned in the readme file of the this github repository.


I hope you found this article helpful. If you have any queries, feel free to comment below and I’ll answer it as soon as I can. Thanks.

Let’s connect!

You can find me on Twitter | LinkedIn | GitHub

Flawless iOS

🍏 Community around iOS development, mobile design, and marketing

Shankar Madeshvaran

Written by

iOS and Xamarin Developer.I love to write articles regarding Techs,iOS and Xamarin concepts. Follow me on GitHub: https://github.com/shankarmadeshvaran

Flawless iOS

🍏 Community around iOS development, mobile design, and marketing

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade