This is my test code for playing video in SwiftUI, the video start playing in first time, but when I update the url for other video it shows like multiple (10-20 times) line of:
“AttributeGraph: cycle detected through attribute”
in Xcode console. From what I see I did not made wrong code for this issue, how ever I also could not find an answer in internet for this logs. The videos are playing after click but each time after updating the url this logs comes out in Xcode.
import SwiftUI
import AVKit
struct ContentView: View {
@State private var AVPlayerURL: URL? = nil
var body: some View {
VStack {
if let AVPlayerURL {
VideoPlayer(player: AVPlayer(url: AVPlayerURL))
}
HStack {
Button("Play video 1") {
AVPlayerURL = Bundle.main.url(forResource: "myVideo1", withExtension: "mp4")
}
Button("Play video 2") {
AVPlayerURL = Bundle.main.url(forResource: "myVideo2", withExtension: "mp4")
}
Button("Play video 3") {
AVPlayerURL = Bundle.main.url(forResource: "myVideo3", withExtension: "mp4")
}
}
}
.padding()
}
}
8
The problem seems to be that we need to let the existing VideoPlayer view disappear (or update itself, not sure how to best word this) before creating a new view for the next video. Given the SwiftUI lifecycle I decided to split the change into two changes so that the view got a chance to update itself before playing the new video.
So first let’s use two State properties
@State private var videoName: String?
@State private var player: AVPlayer?
and then use two onChange
for each of them creating a chain reaction
.onChange(of: videoName) { _, newName in
player?.pause()
if let newName = newName {
player = AVPlayer(url: Bundle.main.url(forResource: newName, withExtension: "mp4")!)
}
}
.onChange(of: player) { _, newPlayer in
if let newPlayer = newPlayer {
newPlayer.play()
}
}
So the first onChange will create a new AVPlayer and the second will play the new video but a cycle has passed between them so the view is now in a correct state and won’t print any warnings.
Of course the buttons will need tp be updated to set the video name
Button("Play video 1") {
videoName = "video1"
}
// and so on...
4
Here I find a way for solving the issue of Xcode logs.
import SwiftUI
import AVKit
@main
struct AVPlayer_TestApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(AVPlayerModel.shared)
}
}
}
struct ContentView: View {
@EnvironmentObject var myAVPlayerModel: AVPlayerModel
var body: some View {
VStack {
Group {
if let unwrappedPlayer: AVPlayer = myAVPlayerModel.player {
VideoPlayer(player: unwrappedPlayer)
}
else {
Color.black
}
}
.cornerRadius(5.0)
ButtonView()
}
.padding()
}
}
struct ButtonView: View {
@EnvironmentObject var myAVPlayerModel: AVPlayerModel
var body: some View {
HStack {
Button("Play video 1") {
myAVPlayerModel.AVPlayerURL = Bundle.main.url(forResource: "myVideo1", withExtension: "mp4")
}
Button("Play video 2") {
myAVPlayerModel.AVPlayerURL = Bundle.main.url(forResource: "myVideo2", withExtension: "mp4")
}
Button("Play video 3") {
myAVPlayerModel.AVPlayerURL = Bundle.main.url(forResource: "myVideo3", withExtension: "mp4")
}
}
}
}
class AVPlayerModel: ObservableObject {
static let shared: AVPlayerModel = AVPlayerModel()
@Published var AVPlayerURL: URL? = nil {
didSet(oldValue) {
if let unwrappedAVPlayerURL: URL = self.AVPlayerURL {
if let unwrappedOldValue: URL = oldValue {
if (unwrappedAVPlayerURL != unwrappedOldValue) {
self.player?.pause()
self.player = nil
}
else {
let seekToZero = CMTime(seconds: 0, preferredTimescale: 60000)
self.player?.seek(to: seekToZero, toleranceBefore: .zero, toleranceAfter: .zero)
return
}
}
else {
self.player?.pause()
self.player = nil
}
DispatchQueue.main.async {
if let unwrappedAVPlayerURL: URL = self.AVPlayerURL {
self.player = AVPlayer(url: unwrappedAVPlayerURL)
self.player?.play()
}
}
}
}
}
@Published var player: AVPlayer? = nil
}