How to access property in SwiftUI wrapper that conforms to UIViewRepresentable?

Viewed 629

I'd like to control an instance of a AVPlayer that is instantiated and assigned to a property in the class VideoPlayer which conforms to UIView. To make sure it's clear what my intent is, I've put together a known pattern when dealing with SwiftUI and UIViewRepresentable wrappers, as follows:

class VideoPlayer: UIView {
    private let playerLayer = AVPlayerLayer()
    private var previewTimer: Timer?
    var previewLength: Double
    var player: AVPlayer?
    
    init(frame: CGRect, url: URL, previewLength:Double) {
        self.previewLength = previewLength
        
        super.init(frame: frame)
        self.player = AVPlayer(url: url)
        self.player?.volume = 0
        self.player?.play()

        NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem, queue: .main) { [weak self] _ in
            self?.player?.seek(to: CMTime.zero)
            self?.player?.play()
        }
        
        playerLayer.player = player
        playerLayer.videoGravity = .resizeAspectFill
        playerLayer.backgroundColor = UIColor.black.cgColor
        
        previewTimer = Timer.scheduledTimer(withTimeInterval: previewLength, repeats: true, block: { (timer) in
            self.player?.seek(to: CMTime(seconds: 0, preferredTimescale: CMTimeScale(1)))
        })
        
        layer.addSublayer(playerLayer)
    }
    
    required init?(coder: NSCoder) {
        self.previewLength = 15
        super.init(coder: coder)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer.frame = bounds
    }
}

The wrapper MyWrapper exposes the UIView to SwiftUI, and the wrapper can have public properties for example myState:

struct MyWrapper: UIViewRepresentable {
    var videoURL: URL
    var previewLength: Double
    @Binding var mySate: Bool

    func makeUIView(context: Context) -> UIView {
        let videoPlayer = VideoPlayer(frame: .zero, url: videoURL, previewLength: previewLength)
        return videoPlayer
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        if myState {
          // do something
        } else {
          // do something else
        }
    }
}

If required to control a property of the instance VideoPlayer, we might be tempted to access properties directly from the UIView object in the method updateUIView. So, let's say that hypothetically we want to access the player property:

func updateUIView(_ uiView: UIView, context: Context) {
    if myState {
      uiView.player.play()
    }
}

This fails with the error Value of type 'UIView' has no member 'player'.

To overcome this created a proxy in the MyWrapper to reference the AVPlayer instance, as follows:

struct MyWrapper: UIViewRepresentable {
    var videoURL: URL
    var previewLength: Double
    @State var player: AVPlayer?
    @Binding var play: Bool

    func makeUIView(context: Context) -> UIView {
        let videoPlayer = VideoPlayer(frame: .zero, url: videoURL, previewLength: previewLength)
        DispatchQueue.main.async {
            self.player = videoPlayer.player
        }
        return videoPlayer
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        if play {
            self.player?.play()
        } else {
            self.player?.pause()
            self.player?.rate = 0
        }
    }
}

Obs: We use the DispatchQueue.main to bypass a warning Modifying state during view update, this will cause undefined behaviour that'd be thrown in makeUIView.

This approach works but I haven't found any documentation in Apple confirming that this is the right approach when dealing with SwiftUI and UIViewRepresentable wrappers, as SwiftUI is a black box.

So, for this reason I'd like to know how to access a property in SwiftUI wrapper that conforms to UIViewRepresentable?

1 Answers

UIViewRepresentable infers type, in this case, from declared return/argument type, so just highlight explicitly what type is represented

struct MyWrapper: UIViewRepresentable {
    var videoURL: URL
    var previewLength: Double
    @Binding var mySate: Bool

    func makeUIView(context: Context) -> VideoPlayer {
        let videoPlayer = VideoPlayer(frame: .zero, url: videoURL, previewLength: previewLength)
        return videoPlayer
    }
    
    func updateUIView(_ uiView: VideoPlayer, context: Context) {
        if myState {
          uiView.player.play()    // now it knows that uiView is-a VideoPlayer
        } else {
          // do something else
        }
    }
}
Related