VideoPlayer in SwiftUI stops playing when parent-View updates - swift

Using Swift5.3.2, iOS14.4.1, Xcode 12.4,
I am successfully running a VideoPlayer in SwiftUI.
I am calling the Player view with this code: VideoPlayer(player: AVPlayer(url: url)).
The problem is that the video stops playing whenever a parent-View of the VideoPlayer updates (i.e. re-renders).
Since in SwiftUI I don't have any control over when such a re-render moment takes place, I don't know how to overcome this problem.
Any ideas ?
Here is the entire Code:
The VideoPlayer View is called as such:
struct MediaTabView: View {
#State private var url: URL
var body: some View {
// CALL TO VIDEOPLAYER IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
VideoPlayer(player: AVPlayer(url: url))
}
}
The MediaTabView is called as such:
import SwiftUI
struct PageViewiOS: View {
var body: some View {
ZStack {
Color.black
// CALL TO MEDIATABVIEW IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!d
MediaTabView(url: URL(string: "https://someurel.com"))
CloseButtonView()
}
}
}
The PageViewiOS View is called as such:
struct MainView: View {
#EnvironmentObject var someState: AppStateService
var body: some View {
NavigationView {
Group {
if someState = .stateOne {
// CALL TO PAGEVIEWIOS IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
PageViewiOS()
} else {
Text("hello")
}
}
}
}
}

This is in response to our comment thread on the other answer:
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer?
func loadFromUrl(url: URL) {
avPlayer = AVPlayer(url: url)
}
}
struct CustomPlayerView: View {
var url : URL
#StateObject private var playerViewModel = PlayerViewModel()
var body: some View {
ZStack {
if let avPlayer = playerViewModel.avPlayer {
VideoPlayer(player: avPlayer)
}
}.onAppear {
playerViewModel.loadFromUrl(url: url)
}
}
}
I'm not sure that this is definitively better, so it's worth testing. But, it does control when AVPlayer gets created and avoids re-creating PlayerViewModel on every render of the parent as well.

With the solution from #jnpdx, everything works now.
Here is the final solution (full credit to #jnpdx):
import SwiftUI
import AVKit
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer?
func loadFromUrl(url: URL) {
avPlayer = AVPlayer(url: url)
}
}
struct CustomPlayerView: View {
var url : URL
#StateObject private var playerViewModel = PlayerViewModel()
var body: some View {
ZStack {
if let avPlayer = playerViewModel.avPlayer {
VideoPlayer(player: avPlayer)
}
}.onAppear {
playerViewModel.loadFromUrl(url: url)
}
}
}
With that in hand, it is enough to call the CustomPlayerVideo like that:
CustomPlayerView(url: url)
Remark: I needed to use ZStack instead of Group in my CustomPlayerView in order for it to work.

I have found a solution.
Call the following :
CustomPlayerView(url: url)
...instead of :
VideoPlayer(player: AVPlayer(url: url))
Not sure why this works, tough.
Maybe somebody can explain further ?
Here is the CustomVideoPlayer code:
struct CustomPlayerView: View {
private let url: URL
init(url: URL) {
self.url = url
}
var body: some View {
VideoPlayer(player: AVPlayer(url: url))
}
}
With this minor change, the Video keeps on playing even tough the parent-View gets re-rendered. Still, not sure why ???
----------- Answer with the hint of #jnpdx --------
I changed the CustomVideoPlayer even more :
CustomPlayerView(playerViewModel: PlayerViewModel(avPlayer: AVPlayer(url: url)))
import SwiftUI
import AVKit
class PlayerViewModel: ObservableObject {
#Published var avPlayer: AVPlayer
init(avPlayer: AVPlayer) {
self.avPlayer = avPlayer
}
}
struct CustomPlayerView: View {
#StateObject var playerViewModel: PlayerViewModel
var body: some View {
VideoPlayer(player: playerViewModel.avPlayer)
}
}

Related

How can I get #AppStorage to work in an MVVM / SwiftUI framework?

I have a SettingsManager singleton for my entire app that holds a bunch of user settings. And I've got several ViewModels that reference and can edit the SettingsManager.
The app basically looks like this...
import PlaygroundSupport
import Combine
import SwiftUI
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
#AppStorage("COUNT") var count = 10
}
class ViewModel: ObservableObject {
#Published var settings = SettingsManager.shared
func plus1() {
settings.count += 1
objectWillChange.send()
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(action: viewModel.plus1) {
Text("\(viewModel.settings.count)")
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Frustratingly, it works about 85% of the time. But 15% of the time, the values don't update until navigating away from the view and then back.
How can I get #AppStorage to play nice with my View Model / MVVM framework?!
Came across this question researching this exact issue. I came down on the side of letting SwiftUI do the heavy lifting for me. For example:
// Use this in any view model you need to update the value
extension UserDefaults {
static func setAwesomeValue(with value: Int) {
UserDefaults.standard.set(value, forKey: "awesomeValue")
}
static func getAwesomeValue() -> Int {
return UserDefaults.standard.bool(forKey: "awesomeValue")
}
}
// In any view you need this value
struct CouldBeAnyView: some View {
#AppStorage("awesomeValue") var awesomeValue = 0
}
AppStorage is just a wrapper for UserDefaults. Whenever the view model updates the value of "awesomeValue", AppStorage will automatically pick it up. The important thing is to pass the same key when declaring #AppStorage. Probably shouldn't use a string literal but a constant would be easier to keep track of?
This SettingsManager in a cancellables set solution adapted from the Open Source ACHN App:
import PlaygroundSupport
import Combine
import SwiftUI
class SettingsManager: ObservableObject {
static let shared = SettingsManager()
#AppStorage("COUNT") var count = 10 {
willSet { objectWillChange.send() }
}
}
class ViewModel: ObservableObject {
#Published var settings = SettingsManager.shared
var cancellables = Set<AnyCancellable>()
init() {
settings.objectWillChange
.sink { [weak self] _ in
self?.objectWillChange.send()
}
.store(in: &cancellables)
}
func plus1() {
settings.count += 1
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Button(action: viewModel.plus1) {
Text(" \(viewModel.settings.count) ")
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Seems to be slightly less glitchy, but still isn't 100% rock-solid consistent :(
Leaving this here to hopefully inspire someone with my attempt

SwiftUI Video player how to set url

I am trying to create a view that allows me to send a URL of the video I want to play. My view looks like this:
import SwiftUI
import AVKit
struct PlayerView: View {
var videoURL : String
private let player = AVPlayer(url: URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!)
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player.pause()
}
}
}
If try to set the URL to the passed parameter I get this error:
Cannot use instance member 'videoURL' within property initializer; property initializers run before 'self' is available
How do I pass aURL to the View to play different movies?
I changed my code as suggested and the video does not play:
import SwiftUI
import AVKit
struct PlayerView: View {
var videoURL : String
#State private var player : AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
guard let url = URL(string: videoURL) else {
return
}
print(url)
let player = AVPlayer(url: url)
self.player = player
self.player?.seek(to: CMTime.zero)
self.player!.play()
}
.onDisappear() {
// Stop the player when the view disappears
player?.pause()
}
}
}
I am trying to change the video on the timer. I tried this:
} else {
VideoPlayer(player: viewModel.player)
.onAppear {
viewModel.urlString = "https://testsite.neumont.edu/images/green.mp4"
}
.onReceive(movieSwitchTimer) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.urlString = "https://testsite.neumont.edu/images/earth.mp4"
}
self.activeImageIndex = (self.activeImageIndex + 1) % slideStore.images.count
}
}
There are a number of ways to achieve this, but since you're already using onAppear, that seems like a good place to accomplish this:
struct PlayerView: View {
var videoURL : String
#State private var player : AVPlayer?
var body: some View {
VideoPlayer(player: player)
.onAppear() {
// Start the player going, otherwise controls don't appear
guard let url = URL(string: videoURL) else {
return
}
let player = AVPlayer(url: url)
self.player = player
player.play()
}
.onDisappear() {
// Stop the player when the view disappears
player?.pause()
}
}
}
You'll want player to be #State because it should persist between renders of the view.
Update, based on comments, handling a scenario where the URL may change:
class VideoViewModel : ObservableObject {
#Published var urlString : String? {
didSet {
guard let urlString = urlString, let url = URL(string: urlString) else {
return
}
player = AVPlayer(url: url)
player.seek(to: .zero)
player.play()
}
}
var player = AVPlayer()
}
struct ContentView : View {
#StateObject var viewModel = VideoViewModel()
var body: some View {
VideoPlayer(player: viewModel.player)
.onAppear {
viewModel.urlString = "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8"
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.urlString = Bundle.main.url(forResource: "IMG_0226", withExtension: "mp4")!.absoluteString
}
}
}
}
In the update, you can see that I store the player in an ObservableObject. Any time the url string gets changed, it reloads the player. The onAppear is not necessary in your code -- it's just a way to show loading different videos at different times (the first being the URL you provided, the second being a URL from my bundle).

Can't pause AVPlayer with custom button

I am trying to make a detailed view in swift, but I just can't figure out a way to pause the video with a custom button. And also when I go back to my list I can still hear the video playing in the background. Here is my code for the AVPlayer and for the button.
import SwiftUI
import AVKit
struct Workdetail: View {
var work: WorkoutDe
#State var player = AVPlayer()
#State var isplaying = true
var body: some View {
VStack {
ZStack {
VideoPlayer(player: $player, work: work)
.frame(height: UIScreen.main.bounds.height / 3.5)
Butto(player: $player, isplaying: $isplaying)
}
Spacer()
}
}
}
struct Butto : View {
#Binding var player : AVPlayer
#Binding var isplaying : Bool
var body : some View {
Button(action: {
if self.isplaying {
self.player.pause()
self.isplaying = false
} else {
self.player.play()
self.isplaying = true
}
}) {
Image(systemName: self.isplaying ? "pause.fill" : "play.fill")
.font(.title)
.foregroundColor(.white)
.padding(20)
}
}
}
struct VideoPlayer : UIViewControllerRepresentable {
var work : WorkoutDe
#Binding var player : AVPlayer
var playerLayer = AVPlayerLayer()
public func makeUIViewController(context: Context) -> AVPlayerViewController {
player = AVPlayer(url: URL(fileURLWithPath: String(work.url)))
let controller = AVPlayerViewController()
controller.player = player
controller.videoGravity = .resizeAspectFill
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in
player.seek(to: CMTime.zero)
player.play()
}
player.play()
return controller
}
func rewindVideo(notification: Notification) {
playerLayer.player?.seek(to: .zero)
}
public func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
}
}
The AVPlayer works but when I press the button nothing happens. The image for the button changes but the video won't stop playing. Can someone please explain to me how I can bind the button, because I can't figure it out
You work with different players in your sub views, try
public func makeUIViewController(context: Context) -> AVPlayerViewController {
let player = AVPlayer(url: URL(fileURLWithPath: String(work.url)))
let controller = AVPlayerViewController()
DispatchQueue.main.async {
self.player = player
}
binding should update parent state, which will update Butto, so it should work.

swiftui dealloc and realloc AVplayer with many videos

I have many simultaneous videos. Through a Int var (var test1 and var test2) I would like to be able to add only a certain video and remove all the others so as not to have memory problems
As soon as the view is loaded, the value "nil" is assigned to each player and when test1 == test 2 it should load the video in a certain player and in the other "nil"
The problem is that despite being the testing variable in binding (struct VideoPlayer #Binding var testing) it does not update the state of the player which always remains in Nil
Below the best solution I have obtained so far
Some idea? Thank you all
struct CustomPlayer: View {
#Binding var test1:Int
#Binding var test2:Int
#State var path:String
#State var testing:AVPlayer? = nil
var body: some View {
if(test1 == test2 ) {
self.testing? = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "\(path)", ofType: "mp4")!) )
self.testing?.play()
} else {
self.testing?.replaceCurrentItem(with: nil)
}
return ZStack{
VideoPlayer(testing: self.$testing)
}
struct VideoPlayer : UIViewControllerRepresentable {
#Binding var testing : AVPlayer?
func makeUIViewController(context: UIViewControllerRepresentableContext<VideoPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = testing
controller.showsPlaybackControls = false
controller.view.backgroundColor = UIColor.white
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
}
}
Here an example how to load multiple videos simultaneous (i added an autoplay feuature), and remove all other, but not the selected one.
To remove videos tap on video you want to save
And i'm not sure that i solve your initial problem, but this can give you a clue where to look next
Code can be copy/pasted, as i declare it in one file to convenient stackoverflow use
import SwiftUI
import AVKit
struct VideoModel: Identifiable {
let id: Int
let name: String
let type: String = ".mp4"
}
final class VideoData: ObservableObject {
#Published var videos = [VideoModel(id: 100, name: "video"),
VideoModel(id: 101, name: "wow"),
VideoModel(id: 102, name: "okay")]
}
//Multiple item player
struct MultipleVideoPlayer: View {
#EnvironmentObject var userData: VideoData
var body: some View {
VStack(alignment: .center, spacing: 8) {
ForEach(userData.videos) { video in
VideoPlayer(video: .constant(video))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
}
}
}
}
//Single item player
struct VideoPlayer : UIViewControllerRepresentable {
#EnvironmentObject var userData: VideoData
#Binding var video: VideoModel
func makeUIViewController(context: Context) -> AVPlayerViewController {
guard let path = Bundle.main.path(forResource: video.name, ofType: video.type) else {
fatalError("\(video.name)\(video.type) not found")
}
let url = URL(fileURLWithPath: path)
let playerItem = AVPlayerItem(url: url)
context.coordinator.player = AVPlayer(playerItem: playerItem)
context.coordinator.player?.isMuted = true
context.coordinator.player?.actionAtItemEnd = .none
NotificationCenter.default.addObserver(context.coordinator,
selector: #selector(Coordinator.playerItemDidReachEnd(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: context.coordinator.player?.currentItem)
let controller = AVPlayerViewController()
controller.player = context.coordinator.player
controller.showsPlaybackControls = false
controller.view.backgroundColor = UIColor.white
controller.delegate = context.coordinator
let tapRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.playerDidTap))
controller.view.addGestureRecognizer(tapRecognizer)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
uiViewController.player?.play()
}
func makeCoordinator() -> Coordinator {
let coord = Coordinator(self)
return coord
}
class Coordinator: NSObject, AVPlayerViewControllerDelegate {
var parent: VideoPlayer
var player: AVPlayer?
init(_ playerViewController: VideoPlayer) {
self.parent = playerViewController
}
#objc func playerItemDidReachEnd(notification: NSNotification) {
if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: CMTime.zero, completionHandler: nil)
}
}
#objc func playerDidTap(){
parent.userData.videos = parent.userData.videos.filter { videoItem in
return videoItem.id == parent.video.id
}
}
}
}
//preview
struct AnotherEntry_Previews: PreviewProvider {
static var previews: some View {
MultipleVideoPlayer()
}
}
And in 'SceneDelegate.swift' replace app entry point with
window.rootViewController = UIHostingController(rootView: MultipleVideoPlayer().environmentObject(VideoData()))
The key thing of this move is to have "VideoData" resource, you can achieve it with EnvironmentObject, or with some other shared data example
Videos below i added to project, including them to Target Membership of a project
#Published var videos = [VideoModel(id: 100, name: "video"),
VideoModel(id: 101, name: "wow"),
VideoModel(id: 102, name: "okay")]
Your code seems to be ok. Are you sure the AVPlayer(...) path is correct
and testing is not nil. Put
.....
self.testing?.play()
print("-----> testing: \(testing.debugDescription)")
is testing = nil at that point?

A link with swift webview application target _blank is not working

I'm a beginner, sorry I did webview with swift, but a link with target _blank doesn't work
I've created a UIViewRepresentable class for the WKWebView from UIKit. The UIViewRepresentable class can be used to create and manage views(UIView) from UIKit in SwiftUI. There are two views in the project,
ContentView lists the urls from string array. Selecting an url navigates to the Detail view.
The Detail view shows the web page for the selected url using WKWebView.
Hope this helps. Here's the code.
import SwiftUI
import UIKit
import WebKit
struct ContentView: View {
var urls: [String] = ["https://www.stackoverflow.com", "https://www.yahoo.com"]
#State private var hideStatusBar = false
var body: some View {
NavigationView {
List {
ForEach(urls, id: \.self) { url in
VStack {
NavigationLink(destination: DetailView(url: url)) {
Text(url)
}
}
}
}
.navigationBarTitle("Main")
}
}
}
struct DetailView: View {
var url: String = ""
var body: some View {
VStack {
Webview(url: url)
Spacer()
}
.navigationBarHidden(true)
}
}
struct Webview: UIViewRepresentable {
var url: String
typealias UIViewType = WKWebView
func makeUIView(context: UIViewRepresentableContext<Webview>) -> WKWebView {
let wkWebView = WKWebView()
guard let url = URL(string: self.url) else {
return wkWebView
}
let request = URLRequest(url: url)
wkWebView.load(request)
return wkWebView
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<Webview>) {
}
}