As of WWDC 2020, we have a way to play videos in SwiftUI without bailing out to UIKit with UIViewRepresentable
. At first glance, it’s pretty simple, as well:
import SwiftUI
import AVKit
struct VideoPlayerView: View {
let videoURL: URL
var body: some View {
let player = AVPlayer(url: videoURL)
VideoPlayer(player: player)
}
}
Et voila, you’re playing a video! You can overlay content on top of the video player pretty easily:
import SwiftUI
import AVKit
struct VideoPlayerView: View {
let videoURL: URL
var body: some View {
let player = AVPlayer(url: videoURL)
VideoPlayer(player: player) {
Text("Watermark")
}
}
}
Seems like we’re good to go, no?
Well, not quite. Let’s talk memory management.
VideoPlayerView
is a struct – it’s immutable. SwiftUI allows us to mutate the state of our views with user interaction using things like @State
, thanks to some Compiler Magic.
Every time some aspect of the state changes, SwiftUI calls the body
getter again.
Spotted the catch yet?
We’re declaring the AVPlayer
instance inside the body
getter. That means it gets reinitalized every time body
gets called. Not the best for something that’s streaming a video file over a network.
But wait, we’ve already mentioned the Compiler Magic we can use to persist state: @State
! Let’s try:
import SwiftUI
import AVKit
struct VideoPlayerView: View {
let videoURL: URL
@State var player = AVPlayer(url: videoURL)
var body: some View {
VideoPlayer(player: player)
}
}
Whoops. We’ve got a problem – self
isn’t available during initialization, so we can’t initialize the AVPlayer
like that. Alright, we’ll write our own init:
import SwiftUI
import AVKit
struct VideoPlayerView: View {
let videoURL: URL
@State var player: AVPlayer
init(videoURL: URL) {
self.videoURL = videoURL
self._player = State(initialValue: AVPlayer(url: videoURL))
}
var body: some View {
VideoPlayer(player: player)
}
}
(I suppose we could drop the let videoURL: URL
there, since we’re using it immediately instead of needing to store it, but for consistency’s sake I’m leaving it in.)
Okay, sounds good – except, hang on, @State
is only intended for use with structs, and if we peek at AVPlayer
it’s a class.
Okay, no worries, that’s what @StateObject
is for, one more tweak:
import SwiftUI
import AVKit
struct VideoPlayerView: View {
let videoURL: URL
@StateObject var player: AVPlayer
init(videoURL: URL) {
self.videoURL = videoURL
self._player = StateObject(wrappedValue: AVPlayer(url: videoURL))
}
var body: some View {
VideoPlayer(player: player)
}
}
There, we should be good to go now, right? Right?
Alas, the compiler says no. AVPlayer
doesn’t conform to ObservableObject
, so we’re out of luck.
Fortunately, ObservableObject
is pretty easy to conform to, and we can make our own wrapper.
import SwiftUI
import AVKit
import Combine
class PlayerHolder: ObservableObject {
let player: AVPlayer
init(videoURL: URL) {
player = AVPlayer(url: videoURL)
}
}
struct VideoPlayerView: View {
let videoURL: URL
@StateObject var playerHolder: PlayerHolder
init(videoURL: URL) {
self.videoURL = videoURL
self._player = StateObject(wrappedValue: PlayerHolder(videoURL: videoURL))
}
var body: some View {
VideoPlayer(player: playerHolder.player)
}
}
Phew. At long last, we’ve got a stable way to hold onto a single AVPlayer
instance. And, as a bonus, we can do stuff with that reference:
import SwiftUI
import AVKit
import Combine
class PlayerHolder: ObservableObject {
let player: AVPlayer
init(videoURL: URL) {
player = AVPlayer(url: videoURL)
}
}
struct VideoPlayerView: View {
let videoURL: URL
@StateObject var playerHolder: PlayerHolder
init(videoURL: URL) {
self.videoURL = videoURL
self._player = StateObject(wrappedValue: PlayerHolder(videoURL: videoURL))
}
var body: some View {
VideoPlayer(player: playerHolder.player)
.onAppear {
playerHolder.player.play()
}
}
}
Will start playing the video as soon as the view opens. Similarly, you could add some Button
s, build your own UI on top of the video player, all sorts of fun.
And, from the other end, you can put more logic in the PlayerHolder, as well. Say you need some additional logic to get from a video ID to the actual URL that the AVPlayer
can handle? Try something like this:
class PlayerHolder: ObservableObject {
@Published var player: AVPlayer? = nil
init(videoID: Video.ID) {
NetworkLayer.shared.lookupVideoInformation(videoID) { result in
self.player = AVPlayer(url: result.url)
}
}
}
(Now, that’s not the nice, Combine-y way to do it, but I’ll leave that as an exercise for the reader. Or possibly another post.)