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

Data binding 让AVPlayer的SwiftUI包装在视图消失时暂停,data-binding,swiftui,avplayer,wrapper,Data Binding,Swiftui,Avplayer,Wrapper,TL;DR 似乎不能使用绑定来告诉wrappedAVPlayer停止-为什么不能?“一个奇怪的把戏”对我来说很有用,没有国家约束,但为什么呢 另请参见 我的问题是这样的,但那张海报想要包装一个avplayervewcontroller,我想要通过编程控制播放 还想知道何时调用了updateUIView() 发生了什么(控制台日志如下所示。) 代码如下所示 用户点击“去看电影” MovieView出现并播放视频 这是因为正在调用updateUIView(uux0:context:) 用户点击

TL;DR

似乎不能使用绑定来告诉wrapped
AVPlayer
停止-为什么不能?“一个奇怪的把戏”对我来说很有用,没有国家约束,但为什么呢

另请参见

我的问题是这样的,但那张海报想要包装一个
avplayervewcontroller
,我想要通过编程控制播放

还想知道何时调用了
updateUIView()

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

代码如下所示

  • 用户点击“去看电影”

    • MovieView
      出现并播放视频
    • 这是因为正在调用
      updateUIView(uux0:context:)
  • 用户点击“回家”

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

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

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

我确实使用了一个
@EnvironmentObject
,因此存在一些状态共享

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

它使用其中一个(仍然是例程声明性SwiftUI):

和包装的
ui视图

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") }
}
当然还有视图路由器,基于这个例子

class ViewRouter:ObserveObject{
let objectWillChange=PassthroughSubject()
枚举页面{案例主页,电影}
var page=page.home{didSet{objectWillChange.send(self)}
//声明:应用程序一次不会播放多个视频。
var isPlayingAV=false//不需要didSet。
}
控制台日志

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

>updateUIView()//第一次调用
router.isPlayingAV=false//Vid未播放=>播放它。
onAppear()
updateUIView()//第二次调用
router.isPlayingAV=true//Vid正在播放=>暂停播放。
onDisappear()//事实发生后,我们会清除
控制台日志2(禁用怪异技巧;继续播放)

>updateUIView()//第一次调用
router.isPlayingAV=false
onAppear()
onDisappear()
控制台日志3(尝试使用状态&binding;播放继续)

>updateUIView()
isplay=false
onAppear()
updateUIView()
isplay=true
updateUIView()
isplay=true
onDisappear()
嗯。。。在

更像是一只野虫子,或者。。。缺陷忘掉它,永远不要在发布产品时使用这些东西

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

这里是一个修改和简化方法的演示,它的行为符合预期(没有奇怪的东西,没有组/ZStack限制),并且可以很容易地扩展或改进(在模型/视图模型层)

使用Xcode 11.2/iOS 13.2进行测试

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

导入快捷界面
进口联合收割机
进口AVKit
结构电影视图:视图{
@环境对象变量路由器:视图路由器
//仅用于演示,但可以互换/修改
让playerModel=PlayerViewModel(url:Bundle.main.url(对于资源:“myVid”,扩展名为“mp4”)!)
var body:一些观点{
VStack(){
PlayerView(视图模型:playerModel)
按钮(操作:{self.router.page=.home}){
文本(“回家”)
}
}奥纳佩尔先生{

self.playerModel.player?.play()//蝎子又叮咬了!我感觉不太好,这取决于事件的顺序-不是很被动或声明性的。谢谢你花时间,我花了好几个小时把这个例子精简到一个“小”的规模。你的代码很有效(必须在
SceneDelegate
中添加环境对象)。我正在学习。我不理解你的评论“好吧,让
PlayerModel
成为
@State
变量并在
PlayerView
上附加一个
id
…并不难,但我想我已经试过了,在浏览地图之前,然后再试一次,结果却得到了它(似乎)行为不一样。这在我使用SwiftUI时总是发生。某种iOS滞后?或者只是我。我不清楚你为什么要将
PlayerWebModel
作为
ObserveObject
。这意味着共享和修改它;将它变成一个不可变的结构,与它的一起生死存亡不是更快捷吗ode>MovieView
?正如我之前的评论所指出的,我确实想在其
MovieView
保留时更改模型,但我通过声明它
@State
并完全替换I
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()")
        }
    }
}
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()")
    }
}
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") }
}
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.
}
> 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.
> updateUIView()                // First call
  router.isPlayingAV = false
< updateUIView()
> onAppear()
< onAppear()
                                // No second call.
> onDisappear()
< onDisappear()
> 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()")
}
class DummyClass { } ; let x = DummyClass()
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()
    }
}