swiftuiavkit

SwiftUI VideoPlayer autoplay without leaking AVPlayer


Presenting a VideoPlayer in a sheet

.sheet(isPresented: $showVideo) {
   VideoPlayerView(videoURL: postVideo.videoURL)
}

To get the video to autoplay, I've implemented the following view

struct VideoPlayerView: View {
    private var player: AVPlayer
    
    init(videoURL: URL) {
        player = AVPlayer(url: videoURL)
    }
    
    var body: some View {
        VideoPlayer(player: player)
            .edgesIgnoringSafeArea(.all)
            .onAppear {
                player.play()
            }
            .onDisappear() {
                player.pause()
            }
    }
}

However, checking the Memory Graph, the AVPlayer is being leaking. Omitting the onDisappear() player.pause() line, the video's sound keeps playing even after dismissing the sheet.

Going with the simpler version that doesn't keep an instance of AVPlayer doesn't have those issues, but then I lose autoplay.

struct VideoPlayerView: View {
    let videoURL: URL
    
    var body: some View {
        VideoPlayer(player: AVPlayer(url: videoURL))
            .edgesIgnoringSafeArea(.all)
    }
}

What would be the correct way of achieving autoplay and not leaking?

(Not looking for UIViewRepresentable solution)


Solution

  • You can't init objects in body because View structs are just values with no lifetime so any objects you init will just get lost. Instead, your objects need to be init in actions like .onAppear, .onChange etc. and stored inside a property wrapper like @State - which essentially is another object that is given to the same View every time it is re-init, which you can do as follows:

    struct VideoPlayerView: View {
        @State var player: AVPlayer? // the same value is given to the VideoPlayerView every time it is init
        let url: URL
    
      var body: some View {
          Group {
            if let player {
                VideoPlayer(player: player)
                .edgesIgnoringSafeArea(.all)
                .onAppear {
                    player.play()
                }
                .onDisappear() {
                    player.pause()
                }
            }
            else {
                Text("Invalid") // usually onChange will only occur the first time if there is something can can appear so this is just a placeholder but you can try without this.
            }
          }
          .onChange(of: url, initial: true) { // fyi initial means on every appear
              if player?.url != url {
                  player = AVPlayer(url: url) // setting this state causes body to be called and a new VideoPlayer to be init with this new player
              }  
          }
    

    In case you're wondering how on earth does SwiftUI know which View to give each @State value back to - it uses it's path in the View hierarchy to identify it, so you have to take care not to move it to a different line in body. The .id modifier can be used to override the automatic path id.