iosswiftuiavfoundationobservableobjectobservation

SwiftUI observe changes in computed property


I have the following computed property (currentTime) in my Observable class which I want to observe in SwiftUI view. Problem is changes in the property are not published as it does not rely on stored properties. I have been using Combine publishers previously to solve the issue but I wonder if there is a better way to solve this in the new observation framework introduced in iOS 17?

   @available(iOS 17.0, *)
   @Observable
    class VideoPlayerVM {
    private(set) public var player: AVPlayer = AVPlayer()


    var currentTime:CMTime {
        let time = self.player?.currentItem?.currentTime()

        if time.isValid {
           return time
        }

        return .invalid
    }

  }

Solution

  • Whatever Combine publisher you were using before should also work with @Observable. You just need to sink that publisher and assign its values to a stored property in the @Observable class.

    That said, I'm not sure why you'd be using a Combine publisher in the first place, if you want to observe the current time of an AVPlayer. There is a dedicated method, addPeriodicTimeObserver, that allows you to observe the current time at regular intervals.

    You can use it like this:

    @Observable
    @MainActor
    class VideoPlayerVM {
        private(set) public var player: AVPlayer = AVPlayer()
        
        
        var currentTime: CMTime = .zero
        private var observation: Any?
        
        func startObservingCurrentTime() {
            guard observation == nil else { return }
            observation = player.addPeriodicTimeObserver(
                forInterval: .init(
                    seconds: 0.5,
                    preferredTimescale: CMTimeScale(NSEC_PER_SEC)
                ),
                queue: nil
            ) { time in
                MainActor.assumeIsolated {
                    self.currentTime = time
                }
            }
        }
        
        func stopObservingCurrentTime() {
            if let observation {
                player.removeTimeObserver(observation)
                observation = nil
            }
        }
    }
    

    You should call stopObservingCurrentTime in onDisappear of the view that owns the VideoPlayerVM.

    Note that I used MainActor.assumeIsolated in the closure for addPeriodicTimeObserver. This is safe because queue: nil means that the closure will be called on the main dispatch queue, which is the serial executor of the main actor.