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

问题描述 投票:0回答:4

正确的 UIKit 方法:

根据 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达到同样的效果?:

为了使用 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 中创建一个自定义的空视图控制器,它只是通过
    AVPlayerViewController
    方法以模态方式呈现我的
    viewDidAppear
    。这使播放器呈现全屏,但它还在显示视频之前显示一个空的视图控制器,我不希望用户看到它。

任何想法将不胜感激!

swiftui presentmodalviewcontroller avplayerviewcontroller avkit
4个回答
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()
    }
}

11
投票

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/


5
投票

Xcode 12·iOS 14

→ 使用

.fullScreenCover
代替
.sheet
,就可以开始了。

另请参阅:如何使用 fullScreenCover 呈现全屏模式视图


0
投票

这是我的解决方案,已针对 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)
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.