简体   繁体   中英

SwiftUI + UIViewRepresentable: how would you have the UIViewRepresentable respond to a binding

Context

I am implementing a video player that should expose play , pause mute and unmute functions. In my case it would be ideal if these boolean settings are passed in as bindings. I have a basic UIViewRepresentable working here:

import SwiftUI
import AVKit
import UIKit


//MARK:- video player with play/pause API

struct MediaPlayerFeedItem : UIViewControllerRepresentable {
    
    var urls: [String]
    @Binding var shouldPlay: Bool
    @Binding var shouldMute: Bool

    var vc =  AVPlayerViewController()
    @State var pauseAll: Bool = false
    

    //MARK:- API
        
    // @use: play from beginning
    func play(){
        self._goPlay()
    }
        
    // @use: pause video
    func pause(){
        vc.player?.pause()
    }
    
    // @use respond to mute event
    func mute(){
    }
    
    // @use: respond to umute event
    func unmute(){
    }
    
        
    //MARK:- view
    
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        if shouldPlay {
            _goPlay()
        } else {
            let items = _makeItems()
            if items.count > 0 {
                let player = AVQueuePlayer(items: items)
                vc.player =  player
                vc.showsPlaybackControls = false
                vc.videoGravity = .resizeAspectFill
                vc.player?.seek(to: .zero)
                vc.player?.pause()
            }
        }
        return vc
    }
    
    // @use: this fn will fire when `shouldPlay` is updated. at which point you can
    //       `play` or `pause` the video as desired
    func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
    }
    
    //MARK:- playback

    private func _goPlay(){

        let items = _makeItems()
        guard items.count > 0 else { return }
        
        let player = AVQueuePlayer(items: items)
        vc.player =  player
        vc.showsPlaybackControls = false
        vc.videoGravity = .resizeAspectFill
        vc.player?.seek(to: .zero)
        vc.player?.play()
       
        NotificationCenter.default
            .addObserver(
                forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
                object: items[items.count-1],
                queue: .main
            ){_ in
                self._goPlay()
            }

    }
    
    // @use: make a new set of avplayeritems before each
    // invocation of _goPlay loop
    private func _makeItems() -> [AVPlayerItem] {
        return urls
            .map{ URL(string: $0) }
            .filter{ $0 != nil }
            .map{ AVPlayerItem(url: $0!) }
    }
    
    

}


Use:

  MediaPlayerFeedItem(urls: [url1,url2], shouldPlay: $shouldPlay, shouldMute: .constant(true) )

The issue is that I am not sure how to connect the UIViewRepresentable with its binding values. That is, when shouldPlay change to true, it would be nice if MediaPlayerFeedItem just calls play() by itself.

===================================================

Update

I forgot to mention that in an earlier implementation, I had this:

func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
    if shouldPlay {
        play()
    } else {
        pause()
    }
}

This is where the behavior of MediaPlayerFeedItem is truly baffling. Both updateViewController and its respective functions calls for play or pause fires as expected, but it doesn't seem to do anything in terms of playing or pausing the video.

For example, if init MediaPlayerFeedItem with shouldPlay set to a true value binding, then the video plays regardless of the pause command that happens later. If I init MediaPlayerFeedItem to shouldPlay set to false binding, and then set it it true again, the video does not play at all. The video loads, it just remains paused at the first frame.

I am inclined to say that the issue is in the _goPlay function itself, but the video does play and loop if I just run _goPlay in the makeUIViewController function regardless of whether shouldPlay is set to true.

Per @aheze suggestion, I added print statements in play , pause and _goPlay functions, the behavior is as follows: @aheze when I init MediaFeedItem with false binding, I am getting 3 pausing prints on initialization. And then 10 seconds later when i set shouldPlay to true, I get one play print statement, and no subsequent pause statement. The funnhy thing is that I am getting loop play print statement as well from _goPlay function in the NotificationCenter call, so it means the video is playing .. it's just not rendering. Note I am initializing all new AVPlayerItem list on each loop, this is needed because i cannot share them across different playing loops.

Make sure to check out the comments:

// @use: this fn will fire when `shouldPlay` is updated. at which point you can
//       `play` or `pause` the video as desired
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {

As the comments say, whenever shouldPlay is updated, updateUIViewController will be called. So, you could do something like this:

func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
    if shouldPlay {
        play()
    } else {
        pause()
    }
}

Figured it out:

struct VideoPlayerView: UIViewRepresentable {

    var urls: [URL]
    var playOnLoad: Bool
    @EnvironmentObject var appState: AppState

    
    func makeUIView(context: Context) -> UIView {
        return PlayerUIViewQueue(urls: urls, playOnLoad: playOnLoad, frame: .zero)
    }
    
    // @use: on `appState` context change, `play` `pause` or `resume` the video
    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {

        if let uiv = uiView as? PlayerUIViewQueue {
            
            switch appState.getTokenPlayCommand() {
            case .pause:
                uiv.pause()
            case .play:
                uiv.playFromBeginning()
            case .resume:
                uiv.resume()
            }
        }
    }

}


class PlayerUIViewQueue: UIView {
    
    private var URLs: [URL]

    // player references
    private let playerLayer = AVPlayerLayer()
    private var current_mutli : AVQueuePlayer? = nil
    private var current_single: AVPlayer? = nil
    
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

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


    init(urls: [URL], playOnLoad:Bool, frame: CGRect){
        
        self.URLs = urls

        super.init(frame: frame)
        
        if self.URLs.count == 1 {
            print("if case")
            initSinglePlayer()
        } else {
            print("else case")
            loopAll()
        }
    }
    

    //MARK:- API
    
    func resume(){
        print("RESUMNING")
        current_mutli?.play()
        current_single?.play()
    }
    
    func pause(){
        print("PAUSING")
        current_mutli?.pause()
        current_single?.pause()
    }
    
    func playFromBeginning(){
        print("playFromBeginning")
        current_mutli?.seek(to: .zero)
        current_mutli?.play()
        current_single?.seek(to: .zero)
        current_single?.play()
    }
    
    
    //MARK:- player utils
    
    private func initSinglePlayer(){
        
        if URLs.count == 0 { return }

        let player = AVPlayer(url: URLs[0])
        player.actionAtItemEnd = .none
        self.current_single = player

        playerLayer.player = player
        playerLayer.videoGravity = .resizeAspectFill

        layer.addSublayer(playerLayer)

        player.play()
        NotificationCenter.default
            .addObserver(
                self,
                selector: #selector(loopPlayerSingle(notification:)),
                name: .AVPlayerItemDidPlayToEndTime,
                object: player.currentItem
            )
        
    }
    
    
    @objc func loopPlayerSingle(notification: Notification) {
        if let playerItem = notification.object as? AVPlayerItem {
            playerItem.seek(to: .zero, completionHandler: nil)
        }
    }

    
    // @use: on each `loopAll()` invocation, reinit
    // the player with all video `items`
    private func loopAll(){

        let items  = URLs.map{ AVPlayerItem.init(url: $0) }
        let player = AVQueuePlayer(items: items)
        self.playerLayer.player = player
        self.current_mutli = player

        player.seek(to: .zero)
        player.play()
        
        playerLayer.videoGravity = .resizeAspectFill
        layer.addSublayer(playerLayer)
        
        NotificationCenter.default
            .addObserver(
                self,
                selector: #selector(loopPlayerMulti(notification:)),
                name: .AVPlayerItemDidPlayToEndTime,
                object: player.items().last
            )
        
    }
    

    
    @objc private func loopPlayerMulti(notification: Notification) {
        loopAll()
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM