Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: How to properly present AVPlayerViewController modally?

Proper UIKit Approach:

According to Apple's WWDC 2019 talk on the subject, AVPlayerViewController should be presented modally to take advantage of all the latest full-screen features of the API. This is the recommended sample code to be called from your presenting UIKit view controller:

// Create the player
let player = AVPlayer(url: videoURL)

// Create the player view controller and associate the player
let playerViewController = AVPlayerViewController()
playerViewController.player = player

// Present the player view controller modally
present(playerViewController, animated: true)

This works as expected and launches the video in beautiful full-screen.

Achieve the Same Effect with SwiftUI?:

In order to use the AVPlayerViewController from SwiftUI, I created the UIViewControllerRepresentable implementation:

struct AVPlayerView: UIViewControllerRepresentable {

    @Binding var videoURL: URL

    private var player: AVPlayer {
        return AVPlayer(url: videoURL)
    }

    func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
        playerController.player = player
        playerController.player?.play()
    }

    func makeUIViewController(context: Context) -> AVPlayerViewController {
        return AVPlayerViewController()
    }
}

I cannot seem to figure out how to present this directly from SwiftUI in the same way as the AVPlayerViewController is presented directly from UIKit. My goal is simply to get all of the default, full-screen benefits.

So far, the following has not worked:

  • If I use a .sheet modifier and present it from within the sheet, then the player is embedded in a sheet and not presented full-screen.
  • I have also tried to create a custom, empty view controller in UIKit that simply presents my AVPlayerViewController modally from the viewDidAppear method. This gets the player to take on the full screen, but it also shows an empty view controller prior to display the video, which I do not want the user to see.

Any thoughts would be much appreciated!

like image 250
josephap Avatar asked Sep 20 '19 19:09

josephap


2 Answers

The solution explained by Razib-Mollick was a good start for me, but it was missing the use of the SwiftUI .sheet() method. I have added this by adding the following to ContentView:

    @State private var showVideoPlayer = false

    var body: some View {
        Button(action: { self.showVideoPlayer = true }) {
            Text("Start video")
        }
        .sheet(isPresented: $showVideoPlayer) {
            AVPlayerView(videoURL: self.$vURL)
                .edgesIgnoringSafeArea(.all)
        }
    }

But the problem is then, that the AVPlayer is instantiated again and again when SwiftUI re-renders the UI. Therefore the state of the AVPlayer has to move to a class object stored in the environment, so we can get hold of it from the View struct. So my latest solution looks now as follows. I hope it helps somebody else.

class PlayerState: ObservableObject {

    public var currentPlayer: AVPlayer?
    private var videoUrl : URL?

    public func player(for url: URL) -> AVPlayer {
        if let player = currentPlayer, url == videoUrl {
            return player
        }
        currentPlayer = AVPlayer(url: url)
        videoUrl = url
        return currentPlayer!
    }
}


struct ContentView: View {
    @EnvironmentObject var playerState : PlayerState
    @State private var vURL = URL(string: "https://www.radiantmediaplayer.com/media/bbb-360p.mp4")

    @State private var showVideoPlayer = false

    var body: some View {
        Button(action: { self.showVideoPlayer = true }) {
            Text("Start video")
        }
        .sheet(isPresented: $showVideoPlayer, onDismiss: { self.playerState.currentPlayer?.pause() }) {
            AVPlayerView(videoURL: self.$vURL)
                .edgesIgnoringSafeArea(.all)
                .environmentObject(self.playerState)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(PlayerState())
    }
}


struct AVPlayerView: UIViewControllerRepresentable {

    @EnvironmentObject var playerState : PlayerState
    @Binding var videoURL: URL?

    func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
    }

    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let playerController = AVPlayerViewController()
        playerController.modalPresentationStyle = .fullScreen
        playerController.player = playerState.player(for: videoURL!)
        playerController.player?.play()
        return playerController
    }
}

Something to be aware of (a bug?): whenever a modal sheet is shown using .sheet() the environment objects are not automatically passed to the subviews. They have to be added using environmentObject(). Here is a link to read more about this problem: https://oleb.net/2020/sheet-environment/

like image 193
pd95 Avatar answered Nov 12 '22 22:11

pd95


Xcode 12 · iOS 14

→ Use .fullScreenCover instead of .sheet and you’re good to go.

See also: How to present a full screen modal view using fullScreenCover

like image 29
ixany Avatar answered Nov 13 '22 00:11

ixany