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

Getting SwiftUI wrapper of AVPlayer to pause when view disappears

TL;DR

似乎无法使用绑定来告诉包装 AVPlayer 停止 — 为什么不呢? Vlad 中的 "one weird trick" 对我有用,没有状态和绑定,但为什么呢?

另请参阅

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

This guy 也想知道什么时候调用了 updateUIView()

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

使用此处所示的代码,

代码

使用@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,wrappER:

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

嗯...在

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

这在删除 视图后称为(类似于didRemoveFromSuperview,而不是will...),所以我什么都看不到bad/wrong/unexpected 中的子视图(甚至它本身)没有更新(在这种情况下 updateUIView)...如果是这样我会感到惊讶(为什么更新视图, 哪个不在视图层次结构中?!)。

所以这个

class DummyClass { } ; let x = DummyClass()

是一些 野生 错误,或...错误。算了,别再用这种东西发产品了。

好的,现在有人会问,这个怎么办?我在这里看到的主要问题是由设计引起的,特别是 PlayerUIView 中模型和视图的紧密耦合,因此无法管理工作流。 AVPlayer 这里不是视图的一部分 - 它是模型并根据其状态 AVPlayerLayer 绘制内容。因此,解决方案是将这些实体分开并分别管理:一个视图一个视图,一个模型一个模型。

这是一个修改和简化方法的演示,它的行为符合预期(w/o 奇怪的东西和 w/o Group/ZStack 限制),并且可以很容易地扩展或改进它(在 model/viewmodel 层)

测试 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()
            }
        }
    }
}

backup