让 AVPlayer 的 SwiftUI 包装器在视图消失时暂停

And*_*can 3 data-binding wrapper avplayer swiftui

长话短说

\n\n

似乎无法使用绑定来告诉wrappedAVPlayer停止\xe2\x80\x94 为什么不呢?弗拉德的“一个奇怪的技巧” 对我来说很有效,没有状态和绑定,但为什么呢?

\n\n

也可以看看

\n\n

我的问题与此类似,但该海报想要包装一个AVPlayerViewController,我想以编程方式控制播放。

\n\n

这家伙也想知道什么时候updateUIView()被叫到的。

\n\n

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

\n\n

使用如下所示的代码,

\n\n
    \n
  • 用户点击“去看电影”

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

    \n\n
      \n
    • HomeView再次出现
    • \n
    • 播放停止
    • \n
    • 再次updateUIView被呼叫。
    • \n
    • 请参阅控制台日志 1
    • \n
  • \n
  • 但是...删除该###行,然后

    \n\n
      \n
    • 即使返回主视图,播放也会继续
    • \n
    • updateUIView抵达时会接到电话,但离开时不会接到电话
    • \n
    • 请参阅控制台日志 2
    • \n
  • \n
  • 如果您取消注释%%%代码(并注释掉其前面的内容)

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

代码

\n\n

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

\n\n

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

\n\n
struct HomeView: View {\n    @EnvironmentObject var router: ViewRouter\n\n    var body: some View {\n        ZStack() {  // +++ Weird trick ### fails if this is Group(). Wtf?\n            if router.page == .home {\n                Button(action: { self.router.page = .movie }) {\n                    Text("Go to Movie")\n                }\n            } else if router.page == .movie {\n                MovieView()\n            }\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
struct MovieView: View {\n    @EnvironmentObject var router: ViewRouter\n    // @State private var isPlaying: Bool = false  // %%%\n\n    var body: some View {\n        VStack() {\n            PlayerView()\n            // PlayerView(isPlaying: $isPlaying) // %%%\n            Button(action: { self.router.page = .home }) {\n                Text("Go back Home")\n            }\n        }.onAppear {\n            print("> onAppear()")\n            self.router.isPlayingAV = true\n            // self.isPlaying = true  // %%%\n            print("< onAppear()")\n        }.onDisappear {\n            print("> onDisappear()")\n            self.router.isPlayingAV = false\n            // self.isPlaying = false  // %%%\n            print("< onDisappear()")\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

前述的PlayerViewwrappER:

\n\n
struct PlayerView: UIViewRepresentable {\n    @EnvironmentObject var router: ViewRouter\n    // @Binding var isPlaying: Bool     // %%%\n\n    private var myUrl : URL?   { Bundle.main.url(forResource: "myVid", withExtension: "mp4") }\n\n    func makeUIView(context: Context) -> PlayerView {\n        PlayerUIView(frame: .zero , url  : myUrl)\n    }\n\n    // ### This one weird trick makes OS call updateUIView when view is disappearing.\n    class DummyClass { } ; let x = DummyClass()\n\n    func updateUIView(_ v: PlayerView, context: UIViewRepresentableContext<PlayerView>) {\n        print("> updateUIView()")\n        print("  router.isPlayingAV = \\(router.isPlayingAV)")\n        // print("  isPlaying = \\(isPlaying)") // %%%\n\n        // This does work. But *only* with the Dummy code ### included.\n        // See also +++ comment in HomeView\n        if router.isPlayingAV  { v.player?.pause() }\n        else                   { v.player?.play() }\n\n        // This logic looks reversed, but is correct.\n        // If it\'s the other way around, vid never plays. Try it!\n        //   if isPlaying { v?.player?.play()   }   // %%%\n        //   else         { v?.player?.pause()  }   // %%%\n\n        print("< updateUIView()")\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

和包装UIView

\n\n
class PlayerUIView: UIView {\n    private let playerLayer = AVPlayerLayer()\n    var player: AVPlayer?\n\n    init(frame: CGRect, url: URL?) {\n        super.init(frame: frame)\n        guard let u = url else { return }\n\n        self.player = AVPlayer(url: u)\n        self.playerLayer.player = player\n        self.layer.addSublayer(playerLayer)\n    }\n\n    override func layoutSubviews() {\n        super.layoutSubviews()\n        playerLayer.frame = bounds\n    }\n\n    required init?(coder: NSCoder) { fatalError("not implemented") }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

当然还有基于BLckbirds示例的视图路由器

\n\n
class ViewRouter : ObservableObject {\n    let objectWillChange = PassthroughSubject<ViewRouter, Never>()\n\n    enum Page { case home, movie }\n\n    var page = Page.home { didSet { objectWillChange.send(self) } }\n\n    // Claim: App will never play more than one vid at a time.\n    var isPlayingAV = false  // No didSet necessary.\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

控制台日志

\n\n

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

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

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

\n\n
> updateUIView()                // First call\n  router.isPlayingAV = false\n< updateUIView()\n> onAppear()\n< onAppear()\n                                // No second call.\n> onDisappear()\n< onDisappear()\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
> updateUIView()\n  isPlaying = false\n< updateUIView()\n> onAppear()\n< onAppear()\n> updateUIView()\n  isPlaying = true\n< updateUIView()\n> updateUIView()\n  isPlaying = true\n< updateUIView()\n> onDisappear()\n< onDisappear()\n
Run Code Online (Sandbox Code Playgroud)\n

Asp*_*eri 5

嗯……在

}.onDisappear {
    print("> onDisappear()")
    self.router.isPlayingAV = false
    print("< onDisappear()")
}
Run Code Online (Sandbox Code Playgroud)

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

所以这

class DummyClass { } ; let x = DummyClass()
Run Code Online (Sandbox Code Playgroud)

而是一些野生错误,或者……错误。忘记它吧,永远不要在发布产品时使用这样的东西。

好吧,现在有人会问,该怎么办?我在这里看到的主要问题是设计引起的,特别是模型和视图的紧密耦合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()
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)