Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call a uikit viewcontroller method from swiftui view

I have looked all over for the answer to this question and can't seem to find it. How can I call a viewController method from swiftUI (e.g. on a button click)?

I have a viewcontroller that looks like this:

import Player

class PlayerViewController: UIViewController {
    var player = Player()
    func play() {
        self.player.play()
    }
}

And I have a wrapper that looks like this:

import SwiftUI
import AVFoundation

struct ProjectEditorPlayerBridge: UIViewControllerRepresentable {

    func makeUIViewController(context: Context) -> PlayerViewController {
        let player = PlayerViewController()
        return player
    }
    
    func updateUIViewController(_ uiViewController: PlayerViewController, context: Context) {
    }
    
    typealias UIViewControllerType = PlayerViewController
}

I want to be able to use a button action in swiftUI and call the viewController play method once. I have seen answers that suggest setting state/binding on the wrapper and calling the method in updateUIViewController, but when I do this I see it gets called multiple times, not just once.

like image 451
John M Avatar asked Jun 21 '20 21:06

John M


2 Answers

Here is possible protocol/configurator based approach, which allows to use actions directly that looks more appropriate from code simplicity and readability.

protocol Player { // use protocol to hide implementation
    func play()
}

class PlayerViewController: UIViewController, Player {
    var player = Player()
    func play() {
        self.player.play()
    }
}

struct ProjectEditorPlayerBridge: UIViewControllerRepresentable {
    var configurator: ((Player) -> Void)? // callback

    func makeUIViewController(context: Context) -> PlayerViewController {
        let player = PlayerViewController()

        // callback to provide active component to caller
        configurator?(player)

        return player
    }

    func updateUIViewController(_ uiViewController: PlayerViewController, context: Context) {
    }

    typealias UIViewControllerType = PlayerViewController
}

struct DemoPlayerView: View {
    @State private var player: Player?     // always have current player

    var body: some View {
        VStack {
            ProjectEditorPlayerBridge { self.player = $0 }  // << here !!

            // use player action directly !!
            Button("Play", action: player?.play ?? {})
        }
    }
}

backup

like image 87
Asperi Avatar answered Nov 12 '22 16:11

Asperi


Good question. It seems to me that something is missing from SwiftUI here.

If you only have one of these view controllers in your app, then you could workaround this by having a global PassthroughSubject (or another way to pass an event). Your UIViewController could subscribe to it, and your SwiftUI code could publish clicks to it.

If you don't want to do that, here is another workaround that uses UUID to get rid of those multiple calls you mentioned.

Maybe we'll see new options at WWDC 2020.

struct ContentView: View {
    @State var buttonClickID: UUID? = nil       
    var body: some View {
        VStack {
            Button(action: self.callPlay) { Text("Play") }
            ProjectEditorPlayerBridge(clickID: $buttonClickID)
        }
    }       
    func callPlay() {
        buttonClickID = UUID()
    }
}

struct ProjectEditorPlayerBridge: UIViewControllerRepresentable {

    @Binding var clickID: UUID?        
    
    func makeUIViewController(context: Context) -> PlayerViewController {
        let player = PlayerViewController()
        return player
    }
    
    class Coordinator {
        var previousClickID: UUID? = nil
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    func updateUIViewController(_ uiViewController: PlayerViewController, context: Context) {
        print("Update")
        if clickID != context.coordinator.previousClickID {
            uiViewController.play()
            context.coordinator.previousClickID = clickID
        } else {
            print("Not calling play")
        }
    }
    
    typealias UIViewControllerType = PlayerViewController
}
like image 36
Rob N Avatar answered Nov 12 '22 16:11

Rob N