A look at the video card we'll be making

Update for iOS 14

You may use the tutorial below for creating a Video Card for iOS 13, however a native VideoPlayer has been added for iOS 14. Below is an example on how to use it.

import SwiftUI
import AVKit

struct ContentView: View {
    var body: some View {
        VideoPlayer(player: AVPlayer(url:  URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4")!))
    }
}

All of the code for the VideoCard is posted on my GitHub. Feel free to download or star for later use, and if you'd like to support my further development efforts, please consider subscribing at the top! Thanks for stopping by :)

Getting Started

Go ahead and create a blank project in Xcode, and make sure SwiftUI is selected.

Creating The Custom PlayerView

Go ahead and create a new class PlayerView which subclasses UIView. This will give a basic boilerplate as shown below. Go ahead and delete the code for the draw function. We won't need it this time.

import UIKit

class PlayerView: UIView {

}
  1. At the top go ahead and import AVFoundation and AVKit. We'll need these modules in order to add the player layer in a second.
  2. Next define 3 class variables. The first will be the playerLayer, as an AVPlayerLayer(). This is the layer which will actually be showing the video on the card. The next variable is the previewTimer, a Timer optional which will control the looping behavior of the video. Finally define the previewLength, a Double which defines how long the preview loop is. This is what you should have so far:
import UIKit

class PlayerView: UIView {

    private let playerLayer = AVPlayerLayer()
    private var previewTimer:Timer?
    var previewLength:Double


}
  1. Define a custom init function which takes in a CGRect (frame), URL (video URL), and Double (loop duration). Within the initializer, set the preview length and call UIView's initializer with the frame. Also go ahead and define the failable initializer and define a default loop length of 15.
import UIKit

class PlayerView: UIView {

    private let playerLayer = AVPlayerLayer()
    private var previewTimer:Timer?
    var previewLength:Double
    
    init(frame: CGRect, url: URL, previewLength:Double) {
        self.previewLength = previewLength
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        self.previewLength = 15
        super.init(coder: coder)
    }
}
  1. We're almost done with this class. Hang in there. It only get's easier from here. Next we're going to setup the player within the PlayerView initializer function.
init(frame: CGRect, url: URL, previewLength:Double) {
    self.previewLength = previewLength
    super.init(frame: frame)
    
    // Create the video player using the URL passed in.
    let player = AVPlayer(url: url)
    player.volume = 0 // Will play audio if you don't set to zero
    player.play() // Set to play once created.
    
    // Add the player to our Player Layer
    playerLayer.player = player
    playerLayer.videoGravity = .resizeAspectFill // Resizes content to fill whole video layer.
    playerLayer.backgroundColor = UIColor.black.cgColor

    previewTimer = Timer.scheduledTimer(withTimeInterval: previewLength, repeats: true, block: { (timer) in
        player.seek(to: CMTime(seconds: 0, preferredTimescale: CMTimeScale(1)))
    })
    
}

Most of the new code explains itself. However let's explain the timer. The scheduleTimer function takes in our previewLength variable to run a closure after its specified amount of time. That's when we set the player's current time to zero. Rememember the player was already playing (We started it earlier in the initializer).

  1. The final touch on this class is to override the layoutSubviews() function. This will make sure the playerLayer fills out the entire view when it's resized within its SwiftUI View. Final class shown below.
import UIKit
import AVFoundation
import AVKit

class PlayerView: UIView {
    private let playerLayer = AVPlayerLayer()
    private var previewTimer:Timer?
    var previewLength:Double
    
    init(frame: CGRect, url: URL, previewLength:Double) {
        self.previewLength = previewLength
        super.init(frame: frame)
        
        // Create the video player using the URL passed in.
        let player = AVPlayer(url: url)
        player.volume = 0 // Will play audio if you don't set to zero
        player.play() // Set to play once created
        
        // Add the player to our Player Layer
        playerLayer.player = player
        playerLayer.videoGravity = .resizeAspectFill // Resizes content to fill whole video layer.
        playerLayer.backgroundColor = UIColor.black.cgColor
        
        previewTimer = Timer.scheduledTimer(withTimeInterval: previewLength, repeats: true, block: { (timer) in
            player.seek(to: CMTime(seconds: 0, preferredTimescale: CMTimeScale(1)))
        })
        

        layer.addSublayer(playerLayer)
    }
    
    required init?(coder: NSCoder) {
        self.previewLength = 15
        super.init(coder: coder)
    }

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

Setting Up The UIViewRepresentable

Next up we will creat a wrapper for our PlayerView that you use to integrate into your SwiftUI view hierarchy.

  1. Go ahead and create the class, VideoView, and subclass UIViewRepresentable.
  2. Import the AVFoundation and AVKit modules like last time.
  3. Create 3 class variables: the videoURL:URL, showPreview:Bool, and previewLength:Double
  4. Then UIViewRepresentable requires us to provide implementations of makeUIViewandupdateUIViewfunctions. I've shown below how to implementmakeUIView. updateUIView` will be empty.
import SwiftUI
import AVFoundation
import AVKit

struct VideoView: UIViewRepresentable {
    
    var videoURL:URL
    var previewLength:Double?
    
    func makeUIView(context: Context) -> UIView {
        return PlayerView(frame: .zero, url: videoURL, previewLength: previewLength ?? 15)
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        
    }
}

That's it!
This will create your basic Video View that you can drop into your SWiftUI project. Below is a basic implementation using SwiftUI.
However, we're going to take it a step further if you keep reading

import SwiftUI

struct VideoCardTestView: View {
    
    @State var maxHeight:CGFloat = 200
    
    var body: some View {
        VStack{
            VideoView(videoURL: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4")!, previewLength: 60)
                .cornerRadius(15)
                .frame(width: nil, height: maxHeight, alignment: .center)
                .shadow(color: Color.black.opacity(0.7), radius: 30, x: 0, y: 2)
                .padding(.horizontal, 20)
                .padding(.top, 20)

            Spacer()
        }
    }
}

Extra Credit!

For those sticking with us, I'll show you how we can add a 'play' icon on top of the video, as well as add support for tap gestures.

  1. Create a new SwiftUI View named VideoCard.
  2. Add a @State variable for showing the play icon. Then one for the video URL, and finally the preview length.
@State var videoURL:URL
@State var showPlayIcon:Bool
var previewLength:Double
  1. In the body create a ZStack as the root element and inside place an instance of the VideoView we created earlier.
  2. Now for the play icon. We can create it by instantiating an Image object and provide the string for the system image play icon. place this below your VideoView
Image(systemName: "play.circle.fill")

Add an if statement around the image to conditionally show the play image, by utilizing the showPlayIcon variable. Optionally, you may add some style to the play icon. Here's what your class should generally look ike now:

import SwiftUI

struct VideoCard: View {
    
    @State var videoURL:URL
    @State var showPlayIcon:Bool
    var previewLength:Double
    
    var body: some View {
        ZStack {
            VideoView(videoURL: videoURL, previewLength: previewLength)
            if showPlayIcon {
                Image(systemName: "play.circle.fill")
                .resizable()
                .scaledToFit()
                .frame(minWidth: 20, idealWidth: 40, maxWidth: 40, minHeight: 20, idealHeight: 40, maxHeight: 40, alignment: .center)
                .foregroundColor(Color.white)
            }
            
        }
    }
}

...and for the cherry on top. To add tap gestures to the card, update your view to include this at the bottom of the ZStack

.onTapGesture {
    // You Tapped the Video Card!
}

Extra Credit Complete!

Here's an example mockup of how I used my VideoCard in testing!