让 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()
。
发生了什么(如下所示的控制台日志。)
使用此处所示的代码,
用户点击 "Go to Movie"
MovieView
出现并且视频播放
- 这是因为
updateUIView(_:context:)
被调用
用户点击 "Go back Home"
HomeView
重现
- 播放停止
- 再次调用
updateUIView
。
- 查看控制台日志 1
但是...删除###
行,然后
- 即使主视图returns
也会继续播放
updateUIView
到达时被调用但离开时不被调用
- 查看控制台日志 2
如果您取消注释 %%%
代码(并注释掉它前面的内容)
- 你得到的代码我认为在逻辑上和惯用上都是正确的 SwiftUI...
- ...但是 "it doesn't work"。 IE。视频在到达时播放,但在离开时继续播放。
- 查看控制台日志 3
代码
我做使用@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()
}
}
}
}
TL;DR
似乎无法使用绑定来告诉包装 AVPlayer
停止 — 为什么不呢? Vlad 中的 "one weird trick" 对我有用,没有状态和绑定,但为什么呢?
另请参阅
我的问题类似于 this one,但发帖人想要包装一个 AVPlayerViewController
而我想以编程方式控制播放。
This guy 也想知道什么时候调用了 updateUIView()
。
发生了什么(如下所示的控制台日志。)
使用此处所示的代码,
用户点击 "Go to Movie"
MovieView
出现并且视频播放- 这是因为
updateUIView(_:context:)
被调用
用户点击 "Go back Home"
HomeView
重现- 播放停止
- 再次调用
updateUIView
。 - 查看控制台日志 1
但是...删除
###
行,然后- 即使主视图returns 也会继续播放
updateUIView
到达时被调用但离开时不被调用- 查看控制台日志 2
如果您取消注释
%%%
代码(并注释掉它前面的内容)- 你得到的代码我认为在逻辑上和惯用上都是正确的 SwiftUI...
- ...但是 "it doesn't work"。 IE。视频在到达时播放,但在离开时继续播放。
- 查看控制台日志 3
代码
我做使用@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()
}
}
}
}