Search code examples
iosswiftuiavfoundationavkit

How to manage AVPlayer state in SwiftUI


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") }
        }
    }
}

Solution

  • 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") }
            }
        }
    }