【问题标题】:Getting SwiftUI wrapper of AVPlayer to pause when view disappears当视图消失时,让 AVPlayer 的 SwiftUI 包装器暂停
【发布时间】:2020-03-10 21:48:41
【问题描述】:

TL;DR

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

另见

我的问题类似于this one,但该海报想包装AVPlayerViewController,并且我想以编程方式控制播放。

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

会发生什么(控制台日志如下所示。)

使用这里显示的代码,

  • 用户点击“去电影”

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

    • HomeView再次出现
    • 播放停止
    • 再次调用updateUIView
    • 查看控制台日志 1
  • 但是...删除###这一行,然后

    • 即使返回主视图也会继续播放
    • updateUIView 在抵达时被调用,但在离开时未被调用
    • 查看控制台日志 2
  • 如果您取消注释 %%% 代码(并注释掉它前面的内容)

    • 您会得到我认为在逻辑和习惯上正确的 SwiftUI 代码...
    • ...但是“它不起作用”。 IE。视频在抵达时播放,但在离开时继续播放。
    • 查看控制台日志 3

代码

确实使用@EnvironmentObject,所以正在进行一些状态共享。

主要内容视图(这里没有争议):

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

还有 wrappED 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】:

    嗯...继续

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

    这被称为 视图被删除(它类似于 didRemoveFromSuperview,而不是 will...),所以我没有看到任何 错误/错误/意外在那个子视图(甚至它本身)没有更新(在这种情况下updateUIView)......如果是这样,我宁愿感到惊讶(为什么要更新视图,它不在视图层次结构中?!)。

    所以这个

    class DummyClass { } ; let x = DummyClass()
    

    是一些 wild 错误,或者......错误。忘记它,永远不要在发布产品时使用这些东西。

    好的,现在有人会问,如何处理这个?我在这里看到的主要问题是设计起源,特别是 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()
        }
    }
    

    【讨论】:

    • 蝎子又来了!我对依赖事件顺序有一种不好的感觉——不是非常被动或声明性的。感谢您抽出宝贵时间,我花了几个小时将示例精简为“小”尺寸。您的代码有效(必须在SceneDelegate 中添加环境对象)。我正在学习和学习。
    • 直到我在 Apple 文档中阅读了关于 play() 方法的评论“
    • 好吧,让PlayerModel 成为@State 变量并不难id 附加到PlayerView... 但我想我尝试了 first,然后再遍历整个地图,然后再次尝试,只是为了让它(似乎)表现不同。使用 SwiftUI 时,我总是会遇到这种情况。某种iOS滞后?或者只有我。
    • 我不清楚您为什么将PlayerViewModel 设为ObservableObject。这意味着共享和修改它;让它成为一个不可变的结构,它的MovieView 生死攸关,难道不是更 SwiftUI-ish 吗?正如我之前的评论所指出的,我确实想在其MovieView 保留时更改模型,但我通过声明它@State 并完全替换它来做到这一点。 ObservableObject 带来了哪些额外的好处?
    猜你喜欢
    • 1970-01-01
    • 2021-09-02
    • 1970-01-01
    • 2022-08-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多