当视图消失时,使AVPlayer的SwiftUI包装器暂停 [英] Getting SwiftUI wrapper of AVPlayer to pause when view disappears
问题描述
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屋!