当视图消失时,使AVPlayer的SwiftUI包装器暂停 [英] Getting SwiftUI wrapper of AVPlayer to pause when view disappears

查看:123
本文介绍了当视图消失时,使AVPlayer的SwiftUI包装器暂停的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

TL; DR

似乎无法使用绑定来告诉包装好的 AVPlayer 停止-为什么不呢?来自弗拉德(Vlad)的一个奇怪的把戏 为我工作,没有状态&具有约束力,但是为什么呢?

Can't seem to use binding to tell wrapped AVPlayer to stop — why not? The "one weird trick" from Vlad works for me, without state & binding, but why?

另请参见

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

My question is something like this one but that poster wanted to wrap an AVPlayerViewController and I want to control playback programmatically.

这个家伙还想知道何时调用 updateUIView()

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

What happens (Console logs shown below.)

具有如下所示的代码,


  • 用户点击转到电影

  • The user taps "Go to Movie"


  • MovieView 出现并且vid播放

  • 这是因为 updateUIView(_:context:)被称为

  • MovieView appears and the vid plays
  • This is because updateUIView(_:context:) is being called

用户点按回家


  • Ho meView 重新出现

  • 播放暂停

  • 再次 updateUIView 叫。

  • 请参阅控制台日志1

  • HomeView reappears
  • Playback halts
  • Again updateUIView is being called.
  • See Console Log 1

但是。 ..删除 ### 行,然后

But... remove the ### line, and


  • 即使主页视图返回

  • updateUIView 在到达但未离开时被调用

  • 请参阅控制台日志2

  • Playback continues even when the home view returns
  • updateUIView is called on arrival but not departure
  • See Console log 2

如果取消注释 %%% 代码(并添加注释)

If you uncomment the %%% code (and comment out what precedes it)


  • 您得到的代码我认为在逻辑上和习惯上都是正确的SwiftUI ...

  • ...但是它不起作用。即vid在到达时播放,但在离开时继续播放。

  • 请参阅控制台日志3

代码

要做使用 @EnvironmentObject 因此,有 正在进行状态共享。

I do use an @EnvironmentObject so there is some sharing of state going on.

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

Main content view (nothing controversial here):

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

which uses one of these (still routine declarative 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 的东西。我使用克里斯·马什

Now we get into the AVKit-specific stuff. I use the approach described by 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(根据需要停止播放)

Console log 1 (playing stops as desired)

> 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(奇怪的把戏已禁用;继续播放)

Console log 2 (weird trick disabled; playing continues)

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

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

Console log 3 (attempt to use state & binding; playing continues)

> 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 ... ),所以在子视图(甚至它本身)没有更新的情况下,我看不到任何坏/错误/意外 case updateUIView )...如果这样的话,我会很惊讶(为什么要更新视图,它不在视图层次结构中!!)。

this is called after view is removed (it is like didRemoveFromSuperview, not will...), so I don't see anything bad/wrong/unexpected in that subviews (or even it itself) is not updated (in this case updateUIView)... I would rather surprise if it would be so (why update view, which is not in view hierarchy?!).

所以


class DummyClass { } ; let x = DummyClass()


有点野生错误,或...错误。忘了它,永远不要在发布产品时使用这些东西。

is rather some wild bug, or ... bug. Forget about it and never use such stuff in releasing products.

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

OK, one would now ask, how to do with this? The main issue I see here is design-originated, specifically tight-coupling of model and view in PlayerUIView and, as a result, impossibility to manage workflow. AVPlayer here is not part of view - it is model and depending on its states AVPlayerLayer draws content. Thus the solution is to tear apart those entities and manage separately: views by views, models by models.

此处是修改后的&简化的方法,表现出预期的效果(没有怪异的东西,没有组/ ZStack的限制),并且可以轻松地扩展或改进(在模型/视图模型层中)

Here is a demo of modified & simplified approach, which behaves as expected (w/o weird stuff and w/o Group/ZStack limitations), and it can be easily extended or improved (in model/viewmodel layer)

使用Xcode 11.2 / iOS 13.2测试

Tested with Xcode 11.2 / iOS 13.2

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

Complete module code (can be copy-pasted in ContentView.swift in project from template)

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

这篇关于当视图消失时,使AVPlayer的SwiftUI包装器暂停的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆