SwiftUI:如何正确地以模态方式呈现AVPlayerViewController?

18

UIKit 正确的方法:

根据苹果在主题上的 WWDC 2019 演讲AVPlayerViewController 应该模态呈现,以利用 API 的所有最新全屏特性。这是建议从你的 UIKit 呈现视图控制器调用的示例代码:

// 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)

这个工作符合预期,可以在漂亮的全屏中启动视频。

如何使用SwiftUI实现相同效果?:

为了从SwiftUI使用AVPlayerViewController,我创建了UIViewControllerRepresentable的实现:

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

我似乎找不到方法直接从SwiftUI中以与AVPlayerViewController在UIKit中直接呈现的方式呈现它。 我的目标只是获得所有默认的全屏优势。

到目前为止,以下尝试都没有成功:

  • 如果我使用.sheet修饰符并从其中呈现它,则播放器将嵌入在表中而不是全屏呈现。
  • 我也尝试在UIKit中创建一个自定义的空视图控制器,该控制器只是从viewDidAppear方法模态地呈现我的AVPlayerViewController。这使播放器占据了整个屏幕,但它还显示了一个空的视图控制器,然后再显示视频,这不是我想让用户看到的。

如有建议,非常感谢!


您可以将AVPlayerView()叠加在当前视图上。 - SwiftiSwift
你想要真正的全屏覆盖所有内容,还是只覆盖某个视图,因此不包括选项卡栏? - Fabian
3个回答

14

如果你想像UIKit一样全屏显示,可以考虑尝试从ContentView中使用以下代码:

import SwiftUI
import UIKit
import AVKit

struct ContentView: View {
    let toPresent = UIHostingController(rootView: AnyView(EmptyView()))
    @State private var vURL = URL(string: "https://www.radiantmediaplayer.com/media/bbb-360p.mp4")
    var body: some View {
        AVPlayerView(videoURL: self.$vURL).transition(.move(edge: .bottom)).edgesIgnoringSafeArea(.all)
    }
}

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


struct AVPlayerView: UIViewControllerRepresentable {

    @Binding var videoURL: URL?

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

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

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

2
当我将它呈现为 .sheet 时,这对我很有效。该表将显示为 SwiftUI 模态视图,并在顶部留有小间隙。如果您旋转设备或点击左上角的全屏展开按钮,则播放器进入全屏模式。 - Gene Z. Ragan
1
toPresent = UIHostingController 的目的是什么?如果这是一个愚蠢的问题,我很抱歉,因为我刚开始学习 Swift 开发。 - Grabofus

9

Razib-Mollick提供的解决方案对我来说是一个很好的开端,但缺少了使用SwiftUI .sheet()方法。我已经通过将以下内容添加到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)
        }
    }

但问题是,当SwiftUI重新渲染用户界面时,AVPlayer会一次又一次地被实例化。 因此,AVPlayer的状态必须移动到存储在环境中的一个class对象中,以便我们可以从View struct获取它。因此,我的最新解决方案如下所示。希望能帮助其他人。

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

注意事项(一个bug?):每当使用.sheet()显示模式表单时,环境对象不会自动传递到子视图中。必须使用environmentObject()手动添加。

这是有关此问题的更多信息链接:https://oleb.net/2020/sheet-environment/


5

使用fullScreenCover的问题在于,您会失去UIKit中“滑动以解除”控制器的原始功能。现在,您只能按控制器提供的“x”解除按钮。 - Jacolack

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接