How to Stop Playback of Avplayerviewcontroller as UIviewrepresentable After Dismissal

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



Leave a reply



Submit