根据 Apple 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 中的
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 呈现这个 与直接呈现
的方式相同 来自 UIKit。我的目标只是获得所有默认的全屏优势。
AVPlayerViewController
到目前为止,以下方法尚未奏效:
.sheet
修改器并从工作表中呈现它,则播放器将嵌入到工作表中并且不会全屏呈现。 AVPlayerViewController
方法以模态方式呈现我的 viewDidAppear
。这使播放器呈现全屏,但它还在显示视频之前显示一个空的视图控制器,我不希望用户看到它。 任何想法将不胜感激!
如果你想像 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()
}
}
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 重新渲染 UI 时,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
}
}
需要注意的事情(错误?):每当使用
.sheet()
显示模态表时,环境对象不会自动传递到子视图。必须使用 environmentObject()
添加它们。
以下是阅读有关此问题的更多信息的链接:https://oleb.net/2020/sheet-environment/
这是我的解决方案,已针对 iOS 17 进行了测试,并且 支持交互式关闭(拖动即可关闭)。
由于它使用 UIKit 模式表示,这复制了 UIKit 中的确切行为。
@State var player: AVPlayer?
var body: some View {
Text("Hello World")
// One line modifier
.fullScreenVideoPlayer($player)
.onAppear {
// Set player somehow
player = AVPlayer(url: videoURL)
}
}
import SwiftUI
import AVKit
extension View {
func fullScreenVideoPlayer(_ player: Binding<AVPlayer?>, onDismiss: ((AVPlayer?) -> Void)? = nil) -> some View {
self.modifier(FullscreenVideoPlayerModifier(avPlayer: player, onDismiss: onDismiss))
}
}
private struct FullscreenVideoPlayerModifier: ViewModifier {
@Binding var avPlayer: AVPlayer?
var onDismiss: ((AVPlayer?) -> Void)?
func body(content: Content) -> some View {
content
// Using background to inject view into hierarchy without using SwiftUI sheet
// or fullscreen modifier. Presentation is handled via UIKit
.background {
if $avPlayer.wrappedValue != nil {
FullscreenVideoPlayer(player: $avPlayer, onDismiss: onDismiss)
.frame(width: 1, height: 1)
}
}
}
}
/// SwiftUI wrapper for AVPlayerViewControllerHost
private struct FullscreenVideoPlayer: UIViewControllerRepresentable {
@Binding var player: AVPlayer?
var onDismiss: ((AVPlayer?) -> Void)?
func makeUIViewController(context: Context) -> AVPlayerViewControllerHost {
AVPlayerViewControllerHost()
}
func updateUIViewController(_ viewController: AVPlayerViewControllerHost, context: Context) {
// Update the text in the hosting controller
viewController.player = player
viewController.onDismiss = {
// Clear binding, so view won't be presented again after dismissal
player = nil
onDismiss?(player)
}
}
}
/// ViewController that presents the AVPlayerViewController
/// Needed to create fullscreen experience with interactive dismissal.
private final class AVPlayerViewControllerHost: UIViewController {
var player: AVPlayer?
var onDismiss: (()->Void)?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let viewController = DismissDetectingAVPlayerViewController()
viewController.player = player
viewController.onDismiss = onDismiss
// Configure AVPlayerViewController how you need it
viewController.exitsFullScreenWhenPlaybackEnds = true
// so that iPads won't crash
viewController.popoverPresentationController?.sourceView = self.view
// present the view controller
self.present(viewController, animated: true, completion: nil)
}
}
/// Subclass of AVPlayerViewController to allow detecting dimissal
private final class DismissDetectingAVPlayerViewController: AVPlayerViewController {
var onDismiss: (()->Void)?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
onDismiss?()
}
override func viewDidDisappear(_ animated: Bool) {
// Release player after disappearance, to allow other users of the object to use the player again.
// Otherwise it would not allow playing.
player = nil
super.viewDidDisappear(animated)
}
}