Getting SwiftUI wrapper of AVPlayer to pause when view disappears
Well... on
}.onDisappear {
print("> onDisappear()")
self.router.isPlayingAV = false
print("< onDisappear()")
}
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?!).
So this
class DummyClass { } ; let x = DummyClass()
is rather some wild bug, or ... bug. Forget about it and never use such stuff in releasing products.
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.
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)
Tested with Xcode 11.2 / iOS 13.2
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()
}
}
}
}
SwiftUI + UIViewRepresentable: how would you have the UIViewRepresentable respond to a binding
Figured it out:
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: $0) }
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()
}
}
Playback controls in swiftui
VideoPlayer
does not appear to have any methods on it that I can find in the documentation to control whether the playback controls are shown.
showPlaybackControls
doesn't work because AVPlayer
doesn't have that property either.
It looks like for now you'll have to do something like wrap an AVPlayerViewController
:
Note that this is a very limited example and doesn't take into account a lot of scenarios you may need to consider, like what happens when AVPlayerControllerRepresented
gets reloaded because it's parent changes -- will you need to use updateUIViewController
to update its properties? You will also probably need a more stable solution to storing your AVPlayer
than what I used, which will also get recreated any time the parent view changes. But, all of these are relatively easily solved architectural decisions (look into ObservableObject, StateObject, Equatable, etc)
struct ContentView: View {
let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "IMG_0226", ofType: "mp4")!))
var body: some View {
AVPlayerControllerRepresented(player: player)
.onAppear {
player.play()
}
}
}
struct AVPlayerControllerRepresented : UIViewControllerRepresentable {
var player : AVPlayer
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
}
How to present a full screen AVPlayerViewController in SwiftUI
Instead of sheet I would use the solution with ZStack (probably with custom transition if needed), like below
ZStack {
// ... other your content below
if showingDetail { // covers full screen above all
PlayerViewController(videoURL: URL(string: "..."))
.edgesIgnoringSafeArea(.all)
//.transition(AnyTransition.move(edge: .bottom).animation(.default)) // if needed
}
}
Related Topics
How to Change/Modify The Displayed Title of an Nspopupbutton
Why Is There Multiple Collision Calls Sprite Kit Swift
Cllocationmanager and Tvos - Requestwheninuseauthorization() Not Prompting
Turn Off Touch for Whole Screen, Spritekit, How
Applescript Used in My Cocoa MAC App, Stopped Working in Osx 10.14
How to Get Alphabetic Tableview Sections from an Object
How to Decorate Siesta Request with an Asynchronous Task
Swift Custom Response Serializer Is Returning Images at Random
Hit Fatal Error: Unexpectedly Found Nil While Unwrapping an Optional Value (Lldb)
iOS Application Support Directory Exists on Devices by Default
Difference Between Orientation and Rotation in Scnnode
Swift Compound Arithmetic Operation Error
How to Convert Curl Request to Swift Using Alamofire
Convert Data to Dispatchdata in Swift 4
Generic Vector with Cardinality Type Safety
How to Make an Ellipse/Circular UIimage with Transparent Background
Midiclientcreate Does Not Work for 32 Bit Processors in Swift