获取AVPlayer的SwiftUI包装以在视图消失时暂停

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

TL; DR

似乎无法使用绑定来告诉包装好的AVPlayer停止-为什么不呢? Vlad中的“一个怪异技巧”对我有用,没有状态和约束,但是为什么呢?

另见

我的问题是类似this one的,但那个发帖人想包装一个AVPlayerViewController,我想以编程方式控制播放。

[This guy也想知道何时调用updateUIView()

会发生什么(下面显示控制台日志。)

使用如下所示的代码,

  • 用户点击“转到电影”

    • [MovieView出现且视频播放
    • 这是因为正在调用updateUIView(_:context:)
  • 用户点击“返回首页”

    • HomeView重新出现
    • 播放暂停
    • 再次调用updateUIView
    • 请参阅控制台日志1
  • But ...删除###行,然后

    • 即使返回主视图后仍继续播放
    • updateUIView在到达但不是离开时被呼唤
    • 请参阅控制台日志2
  • 如果取消注释%%%代码(并注释掉它前面的内容)

    • 您得到的代码我认为在逻辑上和习惯上是正确的SwiftUI ...
    • ...但是“它不起作用”。即vid在到达时播放,但在离开时继续播放。
    • 请参阅控制台日志3

代码

I do使用@EnvironmentObject,所以is正在进行一些状态共享。

主要内容视图(此处无争议):

struct HomeView: View {
    @EnvironmentObject var router: ViewRouter

    var body: some View {
        ZStack() {  // +++ Weird trick ### fails if this is Group(). Wtf?
            if router.page == .home {
                Button(action: { self.router.page = .movie }) {
                    Text("Go to Movie")
                }
            } else if router.page == .movie {
                MovieView()
            }
        }
    }
}

使用其中之一(仍然是常规的声明性SwiftUI):

struct MovieView: View {
    @EnvironmentObject var router: ViewRouter
    // @State private var isPlaying: Bool = false  // %%%

    var body: some View {
        VStack() {
            PlayerView()
            // PlayerView(isPlaying: $isPlaying) // %%%
            Button(action: { self.router.page = .home }) {
                Text("Go back Home")
            }
        }.onAppear {
            print("> onAppear()")
            self.router.isPlayingAV = true
            // self.isPlaying = true  // %%%
            print("< onAppear()")
        }.onDisappear {
            print("> onDisappear()")
            self.router.isPlayingAV = false
            // self.isPlaying = false  // %%%
            print("< onDisappear()")
        }
    }
}

现在我们进入AVKit特定内容。我使用Chris Mash描述的方法。

上述PlayerView,包装器:

struct PlayerView: UIViewRepresentable {
    @EnvironmentObject var router: ViewRouter
    // @Binding var isPlaying: Bool     // %%%

    private var myUrl : URL?   { Bundle.main.url(forResource: "myVid", withExtension: "mp4") }

    func makeUIView(context: Context) -> PlayerView {
        PlayerUIView(frame: .zero , url  : myUrl)
    }

    // ### This one weird trick makes OS call updateUIView when view is disappearing.
    class DummyClass { } ; let x = DummyClass()

    func updateUIView(_ v: PlayerView, context: UIViewRepresentableContext<PlayerView>) {
        print("> updateUIView()")
        print("  router.isPlayingAV = \(router.isPlayingAV)")
        // print("  isPlaying = \(isPlaying)") // %%%

        // This does work. But *only* with the Dummy code ### included.
        // See also +++ comment in HomeView
        if router.isPlayingAV  { v.player?.pause() }
        else                   { v.player?.play() }

        // This logic looks reversed, but is correct.
        // If it's the other way around, vid never plays. Try it!
        //   if isPlaying { v?.player?.play()   }   // %%%
        //   else         { v?.player?.pause()  }   // %%%

        print("< updateUIView()")
    }
}

和wrapdED UIView

class PlayerUIView: UIView {
    private let playerLayer = AVPlayerLayer()
    var player: AVPlayer?

    init(frame: CGRect, url: URL?) {
        super.init(frame: frame)
        guard let u = url else { return }

        self.player = AVPlayer(url: u)
        self.playerLayer.player = player
        self.layer.addSublayer(playerLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer.frame = bounds
    }

    required init?(coder: NSCoder) { fatalError("not implemented") }
}

当然还有视图路由器,基于Blckbirds示例

class ViewRouter : ObservableObject {
    let objectWillChange = PassthroughSubject<ViewRouter, Never>()

    enum Page { case home, movie }

    var page = Page.home { didSet { objectWillChange.send(self) } }

    // Claim: App will never play more than one vid at a time.
    var isPlayingAV = false  // No didSet necessary.
}

控制台日志

控制台日志1(根据需要停止播放)

> updateUIView()                // First call
  router.isPlayingAV = false    // Vid is not playing => play it.
< updateUIView()
> onAppear()
< onAppear()
> updateUIView()                // Second call
  router.isPlayingAV = true     // Vid is playing => pause it.
< updateUIView()
> onDisappear()                 // After the fact, we clear
< onDisappear()                 // the isPlayingAV flag.

控制台日志2(奇怪的把戏已禁用;继续播放)

> updateUIView()                // First call
  router.isPlayingAV = false
< updateUIView()
> onAppear()
< onAppear()
                                // No second call.
> onDisappear()
< onDisappear()

控制台日志3(尝试使用状态和绑定;继续播放)

> updateUIView()
  isPlaying = false
< updateUIView()
> onAppear()
< onAppear()
> updateUIView()
  isPlaying = true
< updateUIView()
> updateUIView()
  isPlaying = true
< updateUIView()
> onDisappear()
< onDisappear()
data-binding swiftui avplayer wrapper
1个回答
0
投票

Well ... on

}.onDisappear {
    print("> onDisappear()")
    self.router.isPlayingAV = false
    print("< onDisappear()")
}

这称为之后视图已被删除(就像didRemoveFromSuperview,而不是will...),因此在该子视图中我什至看不到任何[[坏/错误/意外]](甚至它本身)没有更新(在这种情况下为updateUIView)...如果这样(为什么更新视图,不在视图层次结构中!),我会感到惊讶。 所以这个

class DummyClass { } ; let x = DummyClass()
是一些

wild

错误,或... bug。忘记它,不要在发布产品时使用这些东西。[好,现在有人问,如何处理?我在这里看到的主要问题是源于设计的,尤其是PlayerUIView中的模型和视图的紧密耦合,因此,无法管理工作流。 AVPlayer这里不是视图的一部分-它是模型,并且取决于其状态AVPlayerLayer绘制内容。因此,解决方案是将这些实体拆开并分别管理:按视图的视图,按模型的视图。

这里是经过修改和简化的方法的演示,其行为符合预期(没有怪异的东西,没有Group / ZStack的限制,并且可以轻松地扩展或改进(在模型/视图模型层中)

使用Xcode 11.2 / iOS 13.2测试

完整的模块代码(可以从模板复制粘贴到项目的ContentView.swift中)

import SwiftUI import Combine import AVKit struct MovieView: View { @EnvironmentObject var router: ViewRouter // just for demo, but can be interchangable/modifiable let playerModel = PlayerViewModel(url: Bundle.main.url(forResource: "myVid", withExtension: "mp4")!) var body: some View { VStack() { PlayerView(viewModel: playerModel) Button(action: { self.router.page = .home }) { Text("Go back Home") } }.onAppear { self.playerModel.player?.play() // << changes state of player, ie model }.onDisappear { self.playerModel.player?.pause() // << changes state of player, ie model } } } class PlayerViewModel: ObservableObject { @Published var player: AVPlayer? // can be changable depending on modified URL, etc. init(url: URL) { self.player = AVPlayer(url: url) } } struct PlayerView: UIViewRepresentable { // just thing wrapper, as intended var viewModel: PlayerViewModel func makeUIView(context: Context) -> PlayerUIView { PlayerUIView(frame: .zero , player: viewModel.player) // if needed viewModel can be passed completely } func updateUIView(_ v: PlayerUIView, context: UIViewRepresentableContext<PlayerView>) { } } class ViewRouter : ObservableObject { enum Page { case home, movie } @Published var page = Page.home // used native publisher } class PlayerUIView: UIView { private let playerLayer = AVPlayerLayer() var player: AVPlayer? init(frame: CGRect, player: AVPlayer?) { // player is a model so inject it here super.init(frame: frame) self.player = player self.playerLayer.player = player self.layer.addSublayer(playerLayer) } override func layoutSubviews() { super.layoutSubviews() playerLayer.frame = bounds } required init?(coder: NSCoder) { fatalError("not implemented") } } struct ContentView: View { @EnvironmentObject var router: ViewRouter var body: some View { Group { if router.page == .home { Button(action: { self.router.page = .movie }) { Text("Go to Movie") } } else if router.page == .movie { MovieView() } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }

© www.soinside.com 2019 - 2024. All rights reserved.