SwiftUI + UIViewRepresentable:如何让 UIViewRepresentable 响应绑定
SwiftUI + UIViewRepresentable: how would you have the UIViewRepresentable respond to a binding
上下文
我正在实现一个视频播放器,它应该公开 play
、pause
mute
和 unmute
函数。在我的例子中,如果这些布尔值设置作为绑定传入将是理想的。我有一个基本的 UIViewRepresentable
在这里工作:
import SwiftUI
import AVKit
import UIKit
//MARK:- video player with play/pause API
struct MediaPlayerFeedItem : UIViewControllerRepresentable {
var urls: [String]
@Binding var shouldPlay: Bool
@Binding var shouldMute: Bool
var vc = AVPlayerViewController()
@State var pauseAll: Bool = false
//MARK:- API
// @use: play from beginning
func play(){
self._goPlay()
}
// @use: pause video
func pause(){
vc.player?.pause()
}
// @use respond to mute event
func mute(){
}
// @use: respond to umute event
func unmute(){
}
//MARK:- view
func makeUIViewController(context: Context) -> AVPlayerViewController {
if shouldPlay {
_goPlay()
} else {
let items = _makeItems()
if items.count > 0 {
let player = AVQueuePlayer(items: items)
vc.player = player
vc.showsPlaybackControls = false
vc.videoGravity = .resizeAspectFill
vc.player?.seek(to: .zero)
vc.player?.pause()
}
}
return vc
}
// @use: this fn will fire when `shouldPlay` is updated. at which point you can
// `play` or `pause` the video as desired
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
//MARK:- playback
private func _goPlay(){
let items = _makeItems()
guard items.count > 0 else { return }
let player = AVQueuePlayer(items: items)
vc.player = player
vc.showsPlaybackControls = false
vc.videoGravity = .resizeAspectFill
vc.player?.seek(to: .zero)
vc.player?.play()
NotificationCenter.default
.addObserver(
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: items[items.count-1],
queue: .main
){_ in
self._goPlay()
}
}
// @use: make a new set of avplayeritems before each
// invocation of _goPlay loop
private func _makeItems() -> [AVPlayerItem] {
return urls
.map{ URL(string: [=10=]) }
.filter{ [=10=] != nil }
.map{ AVPlayerItem(url: [=10=]!) }
}
}
使用:
MediaPlayerFeedItem(urls: [url1,url2], shouldPlay: $shouldPlay, shouldMute: .constant(true) )
问题是我不确定如何将 UIViewRepresentable
与其绑定值联系起来。也就是说,当shouldPlay
变为true时,如果MediaPlayerFeedItem
自己调用play()
就好了。
============================================= ======
更新
我忘了提到在早期的实现中,我有这个:
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if shouldPlay {
play()
} else {
pause()
}
}
这就是 MediaPlayerFeedItem
的行为真正令人费解的地方。 updateViewController
及其各自的函数调用 play
或 pause
按预期触发,但它似乎在播放或暂停视频方面没有做任何事情。
例如,如果 init MediaPlayerFeedItem
和 shouldPlay
设置为真值绑定,则无论稍后发生的暂停命令如何,视频都会播放。如果我将 MediaPlayerFeedItem
初始化为 shouldPlay
设置为 false 绑定,然后再次将其设置为 true,则视频根本无法播放。视频加载,它只是在第一帧保持暂停。
我倾向于说问题出在 _goPlay
函数本身,但是如果我只是 运行 _goPlay
在 makeUIViewController
功能,无论 shouldPlay
是否设置为 true。
根据@aheze 的建议,我在 play
、pause
和 _goPlay
函数中添加了打印语句,行为如下: @aheze 当我初始化 MediaFeedItem
使用错误绑定,我在初始化时得到 3 pausing
打印。然后 10 秒后,当我将 shouldPlay
设置为 true 时,我得到一个 play
打印语句,并且没有后续的 pause
语句。有趣的是,我从 NotificationCenter
调用中的 _goPlay
函数也得到了 loop play
打印语句,所以这意味着视频正在播放..它只是没有渲染。请注意,我正在每个循环中初始化所有新的 AVPlayerItem
列表,这是必需的,因为我无法在不同的播放循环中共享它们。
务必查看评论:
// @use: this fn will fire when `shouldPlay` is updated. at which point you can
// `play` or `pause` the video as desired
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
正如评论所说,每当更新shouldPlay
时,都会调用updateUIViewController
。所以,你可以这样做:
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if shouldPlay {
play()
} else {
pause()
}
}
想通了:
struct VideoPlayerView: UIViewRepresentable {
var urls: [URL]
var playOnLoad: Bool
@EnvironmentObject var appState: AppState
func makeUIView(context: Context) -> UIView {
return PlayerUIViewQueue(urls: urls, playOnLoad: playOnLoad, frame: .zero)
}
// @use: on `appState` context change, `play` `pause` or `resume` the video
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {
if let uiv = uiView as? PlayerUIViewQueue {
switch appState.getTokenPlayCommand() {
case .pause:
uiv.pause()
case .play:
uiv.playFromBeginning()
case .resume:
uiv.resume()
}
}
}
}
class PlayerUIViewQueue: UIView {
private var URLs: [URL]
// player references
private let playerLayer = AVPlayerLayer()
private var current_mutli : AVQueuePlayer? = nil
private var current_single: AVPlayer? = nil
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
init(urls: [URL], playOnLoad:Bool, frame: CGRect){
self.URLs = urls
super.init(frame: frame)
if self.URLs.count == 1 {
print("if case")
initSinglePlayer()
} else {
print("else case")
loopAll()
}
}
//MARK:- API
func resume(){
print("RESUMNING")
current_mutli?.play()
current_single?.play()
}
func pause(){
print("PAUSING")
current_mutli?.pause()
current_single?.pause()
}
func playFromBeginning(){
print("playFromBeginning")
current_mutli?.seek(to: .zero)
current_mutli?.play()
current_single?.seek(to: .zero)
current_single?.play()
}
//MARK:- player utils
private func initSinglePlayer(){
if URLs.count == 0 { return }
let player = AVPlayer(url: URLs[0])
player.actionAtItemEnd = .none
self.current_single = player
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
player.play()
NotificationCenter.default
.addObserver(
self,
selector: #selector(loopPlayerSingle(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem
)
}
@objc func loopPlayerSingle(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: .zero, completionHandler: nil)
}
}
// @use: on each `loopAll()` invocation, reinit
// the player with all video `items`
private func loopAll(){
let items = URLs.map{ AVPlayerItem.init(url: [=10=]) }
let player = AVQueuePlayer(items: items)
self.playerLayer.player = player
self.current_mutli = player
player.seek(to: .zero)
player.play()
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
NotificationCenter.default
.addObserver(
self,
selector: #selector(loopPlayerMulti(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player.items().last
)
}
@objc private func loopPlayerMulti(notification: Notification) {
loopAll()
}
}
上下文
我正在实现一个视频播放器,它应该公开 play
、pause
mute
和 unmute
函数。在我的例子中,如果这些布尔值设置作为绑定传入将是理想的。我有一个基本的 UIViewRepresentable
在这里工作:
import SwiftUI
import AVKit
import UIKit
//MARK:- video player with play/pause API
struct MediaPlayerFeedItem : UIViewControllerRepresentable {
var urls: [String]
@Binding var shouldPlay: Bool
@Binding var shouldMute: Bool
var vc = AVPlayerViewController()
@State var pauseAll: Bool = false
//MARK:- API
// @use: play from beginning
func play(){
self._goPlay()
}
// @use: pause video
func pause(){
vc.player?.pause()
}
// @use respond to mute event
func mute(){
}
// @use: respond to umute event
func unmute(){
}
//MARK:- view
func makeUIViewController(context: Context) -> AVPlayerViewController {
if shouldPlay {
_goPlay()
} else {
let items = _makeItems()
if items.count > 0 {
let player = AVQueuePlayer(items: items)
vc.player = player
vc.showsPlaybackControls = false
vc.videoGravity = .resizeAspectFill
vc.player?.seek(to: .zero)
vc.player?.pause()
}
}
return vc
}
// @use: this fn will fire when `shouldPlay` is updated. at which point you can
// `play` or `pause` the video as desired
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
//MARK:- playback
private func _goPlay(){
let items = _makeItems()
guard items.count > 0 else { return }
let player = AVQueuePlayer(items: items)
vc.player = player
vc.showsPlaybackControls = false
vc.videoGravity = .resizeAspectFill
vc.player?.seek(to: .zero)
vc.player?.play()
NotificationCenter.default
.addObserver(
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: items[items.count-1],
queue: .main
){_ in
self._goPlay()
}
}
// @use: make a new set of avplayeritems before each
// invocation of _goPlay loop
private func _makeItems() -> [AVPlayerItem] {
return urls
.map{ URL(string: [=10=]) }
.filter{ [=10=] != nil }
.map{ AVPlayerItem(url: [=10=]!) }
}
}
使用:
MediaPlayerFeedItem(urls: [url1,url2], shouldPlay: $shouldPlay, shouldMute: .constant(true) )
问题是我不确定如何将 UIViewRepresentable
与其绑定值联系起来。也就是说,当shouldPlay
变为true时,如果MediaPlayerFeedItem
自己调用play()
就好了。
============================================= ======
更新
我忘了提到在早期的实现中,我有这个:
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if shouldPlay {
play()
} else {
pause()
}
}
这就是 MediaPlayerFeedItem
的行为真正令人费解的地方。 updateViewController
及其各自的函数调用 play
或 pause
按预期触发,但它似乎在播放或暂停视频方面没有做任何事情。
例如,如果 init MediaPlayerFeedItem
和 shouldPlay
设置为真值绑定,则无论稍后发生的暂停命令如何,视频都会播放。如果我将 MediaPlayerFeedItem
初始化为 shouldPlay
设置为 false 绑定,然后再次将其设置为 true,则视频根本无法播放。视频加载,它只是在第一帧保持暂停。
我倾向于说问题出在 _goPlay
函数本身,但是如果我只是 运行 _goPlay
在 makeUIViewController
功能,无论 shouldPlay
是否设置为 true。
根据@aheze 的建议,我在 play
、pause
和 _goPlay
函数中添加了打印语句,行为如下: @aheze 当我初始化 MediaFeedItem
使用错误绑定,我在初始化时得到 3 pausing
打印。然后 10 秒后,当我将 shouldPlay
设置为 true 时,我得到一个 play
打印语句,并且没有后续的 pause
语句。有趣的是,我从 NotificationCenter
调用中的 _goPlay
函数也得到了 loop play
打印语句,所以这意味着视频正在播放..它只是没有渲染。请注意,我正在每个循环中初始化所有新的 AVPlayerItem
列表,这是必需的,因为我无法在不同的播放循环中共享它们。
务必查看评论:
// @use: this fn will fire when `shouldPlay` is updated. at which point you can
// `play` or `pause` the video as desired
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
正如评论所说,每当更新shouldPlay
时,都会调用updateUIViewController
。所以,你可以这样做:
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if shouldPlay {
play()
} else {
pause()
}
}
想通了:
struct VideoPlayerView: UIViewRepresentable {
var urls: [URL]
var playOnLoad: Bool
@EnvironmentObject var appState: AppState
func makeUIView(context: Context) -> UIView {
return PlayerUIViewQueue(urls: urls, playOnLoad: playOnLoad, frame: .zero)
}
// @use: on `appState` context change, `play` `pause` or `resume` the video
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {
if let uiv = uiView as? PlayerUIViewQueue {
switch appState.getTokenPlayCommand() {
case .pause:
uiv.pause()
case .play:
uiv.playFromBeginning()
case .resume:
uiv.resume()
}
}
}
}
class PlayerUIViewQueue: UIView {
private var URLs: [URL]
// player references
private let playerLayer = AVPlayerLayer()
private var current_mutli : AVQueuePlayer? = nil
private var current_single: AVPlayer? = nil
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
init(urls: [URL], playOnLoad:Bool, frame: CGRect){
self.URLs = urls
super.init(frame: frame)
if self.URLs.count == 1 {
print("if case")
initSinglePlayer()
} else {
print("else case")
loopAll()
}
}
//MARK:- API
func resume(){
print("RESUMNING")
current_mutli?.play()
current_single?.play()
}
func pause(){
print("PAUSING")
current_mutli?.pause()
current_single?.pause()
}
func playFromBeginning(){
print("playFromBeginning")
current_mutli?.seek(to: .zero)
current_mutli?.play()
current_single?.seek(to: .zero)
current_single?.play()
}
//MARK:- player utils
private func initSinglePlayer(){
if URLs.count == 0 { return }
let player = AVPlayer(url: URLs[0])
player.actionAtItemEnd = .none
self.current_single = player
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
player.play()
NotificationCenter.default
.addObserver(
self,
selector: #selector(loopPlayerSingle(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem
)
}
@objc func loopPlayerSingle(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: .zero, completionHandler: nil)
}
}
// @use: on each `loopAll()` invocation, reinit
// the player with all video `items`
private func loopAll(){
let items = URLs.map{ AVPlayerItem.init(url: [=10=]) }
let player = AVQueuePlayer(items: items)
self.playerLayer.player = player
self.current_mutli = player
player.seek(to: .zero)
player.play()
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
NotificationCenter.default
.addObserver(
self,
selector: #selector(loopPlayerMulti(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player.items().last
)
}
@objc private func loopPlayerMulti(notification: Notification) {
loopAll()
}
}