So you may think we finish the work on the isMuted
property on part 2 of this series? Not quite.
Start the demo app, notice the mute icon on the top right of the video controls, what happens if you tap on that control?
Sure thing it will mute our video, but notice that our toggle command does not update accordingly, it gets out of sync with the state of the underlying video player property.
So how can we sync between the player class and our SwiftUI Video view?
Do you remember the UIViewControllerRepresentable
protocol from part 1, beside the makeViewController()
and createViewController()
methods there is a third method in the protocol: makeCoordinator()
.
The goal of this method is to create a Coordinator
class, that coordinates between our SwiftUI View
and the wrapped UIViewController
, this looks like exactly what we need for our isMuted
property.
Let’s see how can we implement it; start by creating an internal class called VideoCoordinator
:
public class VideoCoordinator: NSObject {
let video: Video
var player: AVPlayer?
init(video: Video) {
self.video = video
}
}
Then we can implement our makeCoordinator()
method:
func makeCoordinator() -> VideoCoordinator {
return VideoCoordinator(video: self)
}
After implementing makeCoordinator
, the coordinator class will be available through the context
argument in the other methods of the protocol. So now we can connect the player class to our video component.
public func makeUIViewController(context: Context) -> AVPlayerViewController {
let videoViewController = AVPlayerViewController()
videoViewController.player = AVPlayer(url: videoURL)
let videoCoordinator = context.coordinator
videoCoordinator.player = videoViewController.player
}
So how can we sync the value of isMuted
between the AVPlayer
object and the Video
component?
If you look at the documentation of AVPlayer
you will see that the properties of this class are KVO observable so that we can set a KVO observer for the muted property. Let’s do this using a dedicated method on the coordinator:
func addKVOObservers(to player: AVPlayer?) {
player?.addObserver(self,
forKeyPath: "muted",
options: [.new, .old],
context:&playerContext
)
}
And then we can use it on the didSet method of our player variable:
var player: AVPlayer? {
didSet {
addKVOObserver(to: player)
}
}
We are just missing the implementation of the observer method:
override open func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
guard keyPath == "muted" else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if let player = player {
video.isMuted.wrappedValue = player.isMuted
}
}
And we are done if you start your demo app you will see that every time you tap on the mute icon on the video overlay our toggle control updates accordingly.

Just one last thing, in order to avoid crashes let’s remove the KVO observation on the deinit
method:
private func removeKVOObservers(from player: AVPlayer?) {
player?.removeObserver(self, forKeyPath: "muted")
}
deinit {
removeKVOObservers(from: player)
}
And now we are done with our isMuted
property.
Join us in part 4 to see how to make this component compatible with macOS.
Leave a Reply