I have a list of URLs in SwiftUI. When I tap an item, I present a full screen video player. I have an @EnvironmentObject
that handles some viewer options (for example, whether to show a timecode). I also have a toggle that shows and hides the timecode (I've only included the toggle in this example as the timecode view doesn't matter) but every time I change the toggle the view is created again, which re-sets the AVPlayer
. This makes sense since I'm creating the player in the view's initialiser.
I thought about creating my own ObserveredObject
class to contain an AVPlayer
but I'm not sure how or where I'd initialise it since I need to give it a URL, which I only know from the initialiser of CustomPlayerView
. I also thought about setting the player as an @EnvironmentObject
but it seems weird to initialise something I might not need (if the user doesn't tap on a URL to start the player).
What is the correct way to create an AVPlayer
to hand to AVKit's VideoPlayer
please? Here's my example code:
class ViewerOptions: ObservableObject {
@Published var showTimecode = false
}
struct CustomPlayerView: View {
@EnvironmentObject var viewerOptions: ViewerOptions
private let avPlayer: AVPlayer
init(url: URL) {
avPlayer = AVPlayer(url: url)
}
var body: some View {
HStack {
VideoPlayer(player: avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
There are a couple of approaches you can take here. You can try them out and see which one suits best for you.
Option 1: As you said you can wrap avPlayer
in a new ObserveredObject
class
class PlayerViewModel: ObservableObject {
@Published var avPlayer: AVPlayer? = nil
}
class ViewerOptions: ObservableObject {
@Published var showTimecode = false
}
@main
struct DemoApp: App {
var playerViewModel = PlayerViewModel()
var viewerOptions = ViewerOptions()
var body: some Scene {
WindowGroup {
CustomPlayerView(url: URL(string: "Your URL here")!)
.environmentObject(playerViewModel)
.environmentObject(viewerOptions)
}
}
}
struct CustomPlayerView: View {
@EnvironmentObject var viewerOptions: ViewerOptions
@EnvironmentObject var playerViewModel: PlayerViewModel
init(url: URL) {
if playerViewModel.avPlayer == nil {
playerViewModel.avPlayer = AVPlayer(url: url)
} else {
playerViewModel.avPlayer?.pause()
playerViewModel.avPlayer?.replaceCurrentItem(with: AVPlayerItem(url: url))
}
}
var body: some View {
HStack {
VideoPlayer(player: playerViewModel.avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 2: You can add avPlayer
to your already existing class ViewerOptions
as an optional property and then initialise it when you need it
class ViewerOptions: ObservableObject {
@Published var showTimecode = false
@Published var avPlayer: AVPlayer? = nil
}
struct CustomPlayerView: View {
@EnvironmentObject var viewerOptions: ViewerOptions
init(url: URL) {
if viewerOptions.avPlayer == nil {
viewerOptions.avPlayer = AVPlayer(url: url)
} else {
viewerOptions.avPlayer?.pause()
viewerOptions.avPlayer?.replaceCurrentItem(with: AVPlayerItem(url: url))
}
}
var body: some View {
HStack {
VideoPlayer(player: viewerOptions.avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 3: Make your avPlayer
a state object this way its memory will be managed by the system and it will not re-set it and keep it alive for you until your view exists.
class ViewerOptions: ObservableObject {
@Published var showTimecode = false
}
struct CustomPlayerView: View {
@EnvironmentObject var viewerOptions: ViewerOptions
@State private var avPlayer: AVPlayer
init(url: URL) {
_avPlayer = .init(wrappedValue: AVPlayer(url: url))
}
var body: some View {
HStack {
VideoPlayer(player: avPlayer)
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}
Option 4: Create your avPlayer
object when you need it and forget it (Not sure this is the best approach for you but if you do not need your player object to perform custom actions then you can use this option)
class ViewerOptions: ObservableObject {
@Published var showTimecode = false
}
struct CustomPlayerView: View {
@EnvironmentObject var viewerOptions: ViewerOptions
private let url: URL
init(url: URL) {
self.url = url
}
var body: some View {
HStack {
VideoPlayer(player: AVPlayer(url: url))
Toggle(isOn: $viewerOptions.showTimecode) { Text("Show Timecode") }
}
}
}