Categories
Uncategorized

Video Based Onboarding Screen in SwiftUI

Hello and welcome to another SwiftUI tutorial! Today we’re going to learn how we can stream and play videos inside a SwiftUI application. While doing this, we are going to create a dynamic, video-based SwiftUI onboarding screen. 

Hint: If you’re looking for a short guide to embed videos into SwiftUI apps, take a look at this tutorial. (coming soon)

This is how our app will look like at the end of this tutorial:

Preparing our UI 🎨

Let’s start by setting up a new Xcode project. Open Xcode and create a new Xcode project. Select “Single View App”, name your project and make sure you select “SwiftUI” as the User Interface mode. 

For preparing the UI of our app we can stick with the default ContentView.swift file. We want the played video to cover the entire screen. But before we implement the video player, we work with a solid black background as a placeholder for it. Therefore, delete the default “Hello, World!” Text view and insert a black Color view instead. 

struct ContentView: View {
    var body: some View {
        Color.black
    }
}


By default, any SwiftUI view’s content stays inside the so-called safe area. This causes our Color view to not touch the upper and lower edge of the screen. However, we can tell our Color view to exceed the boundaries of the safe area by using the .edgesIgnoringSafeArea modifier. You can determine which safe area edges you want to ignore. In our example, we choose .all.

struct ContentView: View {
    var body: some View {
        Color.black
            .edgesIgnoringSafeArea(.all)
    }
}


This is how your ContentView preview should look like now:


On top of our Color view (remember: we just use this as a temporary placeholder for the video player) we want to place the logo of our app and two Text views describing the benefits of our app.

To stack views on top of other views, we use ZStacks. Therefore, wrap the Color view into a ZStack and place a VStack below the Color view.

ZStack {
            Color.black
                .edgesIgnoringSafeArea(.all)
            VStack {
                
            }
        }


Inside the VStack, we place the logo for of our app and both text views for aligning them vertically. We start with inserting an Image view for our app’s logo. In this example, we use the “paperplane.fill” system icon and apply the common modifiers to frame it properly. 

VStack {
                Image(systemName: "paperplane.fill")
                    .resizable()
                    .frame(width: 70, height: 70)
                    .aspectRatio(contentMode: .fit)
                    .foregroundColor(.white)
                    .padding(.bottom, 30)
            }


Below our Image view, we place both Text views and stylise them like this:

VStack {
                Image(systemName: "paperplane.fill")
                    //...
                Text("Explore the World")
                    .font(.largeTitle)
                    .foregroundColor(.white)
                Text("Discover the most amazing places in the world and share your experience with the No. 1 travel community.")
                    .foregroundColor(.white)
                    .frame(maxWidth: 320)
                    .padding(.top, 20)
                    .padding(.bottom, 50)
            }


And this is how your preview should look like now:


What’s missing is the bottom row of buttons that allow the user to either login or register. We want to visually separate this by using a thin white rectangle which we place below our last Text view. 

VStack {
                //...
                Rectangle()
                    .frame(height: 1)
                    .foregroundColor(.white)
            }


Next, view insert another HStack holding the “Login” and the “Signup” Text view. We distribute both Text views equally by inserting Spacer views as well.

HStack {
                Spacer()
                Text("Login")
                    .foregroundColor(.white)
                    .padding(20)
                Spacer()
                Text("Signup")
                    .foregroundColor(.white)
                    .padding(20)
                Spacer()
            }


We want to push the whole “button row” to the bottom while fixing the icon and Text views at the center of our ContentView. To do this, place a Spacer view right above the thin Rectangle and above the Image view. 

VStack {
                Spacer()
                Image(systemName: "paperplane.fill")
                    //...
                Text("Explore the World")
                    //...
                Text("Discover the most amazing places in the world and share your experience with the No. 1 travel community.")
                    //...
                Spacer()
                Rectangle()
                    //...
                HStack {
                    //....
                }
            }


Awesome, we’re done with preparing the UI of our onboarding screen. This is how your preview should look like now:


Next, we will learn how to create a video player in SwiftUI that we can use as the background of the ContentView instead of our black Color view. 

Setting up a video player in SwiftUI 📼

Unfortunately, SwiftUI does not provide a video player on its own. Therefore, we need to rely on using an AVPlayer (which is part of the AVKit framework) in connection with the UIKit. This means that we have to create an AVPlayer which we insert into an UIView. Then we’ll use this UIView to embed it into our SwiftUI ContentView. Don’t worry, this sounds more complicated than it actually is. 

As said, we need a UIView view to hold an AVPlayer. Therefore, create a new File-New-File and choose Swift file. Name this file UIPlayerView. Make sure you import the SwiftUI and the AVKit framework and create a class that adopts the UIView protocol.

import AVKit
import SwiftUI

class UIVideoPlayer: UIView {
    
}


Inside this class, we start with declaring an AVPlayerLayer which actually holds the visual output of the video we will play.


class UIVideoPlayer: UIView {
    
    var playerLayer = AVPlayerLayer()

}


When initialising our UIVideoPlayer view, we first make sure to have a valid url before trying to stream the video from it. To do this, we use a guard-let statement. So, insert the following overriding init method into the UIVideoPlayer class. 

override init(frame: CGRect) {
        super.init(frame: frame)
        
       guard let url = URL(string: "https://github.com/BLCKBIRDS/Video-Based-Onboarding-Screen-in-SwiftUI/blob/master/video.mp4?raw=true") else { return }
    }


When overriding the init function we also need to provide our UIView with the required init?(coder: NSCoder) method. So, insert this function into your UIVideoPlayer class as well:

required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


In our overriding init function, after we safely unwrapped the url, we can create an AVPlayer instance that streams the video from the provided url. We also mute the AVPlayer and tell it to play the video immediately upon initialisation.

override init(frame: CGRect) {
        super.init(frame: frame)
        
        guard let url = //...

        let player = AVPlayer(url: url)
        player.isMuted = true
        player.play()
    }


Next, we add the created player to the playerLayer we created earlier. 

override init(frame: CGRect) {
        super.init(frame: frame)
        
        guard let url = //...

        let player = AVPlayer(url: url)
        player.isMuted = true
        player.play()
      
        playerLayer.player = player
    }


We also want to play the video in fullscreen. To do this, we tell our playerLayer to resize the video within its bounds (the bounds will be equal to the frame of the player view once we embedded it into our SwiftUI view) by using the aspect fill option.

override init(frame: CGRect) {
        super.init(frame: frame)
        
        guard let url = //...

        let player = AVPlayer(url: url)
        player.isMuted = true
        player.play()
      
        playerLayer.player = player
        playerLayer.videoGravity = AVLayerVideoGravity(rawValue: AVLayerVideoGravity.resizeAspectFill.rawValue)

    }


Finally, we add the playerLayer to the UIView. This results in the following init function:

override init(frame: CGRect) {
        super.init(frame: frame)
        
        guard let url = //...

        let player = AVPlayer(url: url)
        player.isMuted = true
        player.play()
      
        playerLayer.player = player
        playerLayer.videoGravity = AVLayerVideoGravity(rawValue: AVLayerVideoGravity.resizeAspectFill.rawValue)
        
        layer.addSublayer(playerLayer)
    }

We need to lay out all the subviews of our UIVideoPlayer to adapt their frames to the UIVideoPlayer bounds (remember: our playerLayer is such a subview). To do this, we insert the third and last override method into our UIVideoPlayer class:

override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer.frame = bounds
    }


That’s it. We successfully created a video player that streams the video from a specific url and that we insert into our SwiftUI view.

Inserting and modifying the PlayerView 👨‍💻

But before we can embed the UIVideoPlayer into our SwiftUI ContentView, we need to embed it into a UIViewRepresentable first since only a representable can be directly inserted into a SwiftUI view.

To do this, create another class called PlayerView adopting the UIViewRepresentable protocol.

struct PlayerView: UIViewRepresentable {

}


The UIViewRepresentable protocol has two mandatory functions: makeUIView and updateUIView. The makeUIView function creates and returns the specified view (in our case that should be the UIViewPlayer) while the updateUIView method gets called every time the state of the view gets updated (roughly speaking). However, we don’t need the functionality of the latter one for our app so we just insert the corresponding protocol stub.

struct PlayerView: UIViewRepresentable {

    func makeUIView(context: Context) -> UIVideoPlayer {
        return UIVideoPlayer()
    }

    func updateUIView(_ uiView: UIVideoPlayer, context: Context) {
        
    }
}


Awesome! The PlayerView now hosts our UIVideoPlayer and we can insert it into our ContentView.

So in your ContentView, replace the Color view with a PlayerView instance.

ZStack {
            PlayerView
                .edgesIgnoringSafeArea(.all)
            VStack {
                //...
            }
        }


Your preview gets filled with a blank color now but if you start a live preview, you’ll notice that the video starts playing!


Let’s customise the appearance of the PlayerView a little bit. First, we want to overlay it with an opaque green color.

PlayerView()
                .overlay(Color.green.opacity(0.4))
                .edgesIgnoringSafeArea(.all)


By doing this, we also see the remaining views of our ContentView while the video does not run.

Finally, we also apply some blur to the PlayerView:

PlayerView()
                .overlay(Color.green.opacity(0.4))
                .blur(radius: 1)
                .edgesIgnoringSafeArea(.all)


If you start a live preview now, you see that the video gets overplayed with a green color and is also a little bit blurred which makes the remaining content stand our more.


Conclusion 🎊

That’s it! You just learned how you can stream and play videos inside your SwiftUI app and how you can use this technique for creating a dynamic onboarding screen. 

Here you can find the source code of this app.

I hope you enjoyed this tutorial! If you want to learn more about SwiftUI, make sure you check out our free SwiftUI Basics eBook and our other tutorials! Also make sure you follow us on Instagram and subscribe to our newsletter to not miss any updates, tutorials and tips about SwiftUI and more!