How to call a uikit viewcontroller method from swiftui view
Asked Answered
L

3

5

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.

Livy answered 21/6, 2020 at 21:8 Comment(2)
I stopped using SwiftUI 1.0 (and likely won't be porting anything until SwiftUI 3.0) but one thing I know will work - and is decidedly not SwiftUI nor Combine - is Notifications. Worked like a charm last August.Dark
Does this answer your question? Call UIKIT function from SWIFTUINanoid
N
7

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 ?? {})
        }
    }
}

Newberry answered 22/6, 2020 at 3:38 Comment(1)
I did it the same way, but it makes SwiftUI issue: Modifying state during view update, this will cause undefined behavior.Agalloch
Q
4

I used to struggle with this problem as well and used the Notification Center. But there is a another way. You can create a @StateObject 'Coordinator' in the SwiftUIView, pass it to the UIViewControllerRepresentable and pass the ViewController back to the Coordinator in viewDidLoad and you can call its functions via the coordinator.

// Create a Coordinator
class BridgingCoordinator: ObservableObject {
    var vc: ViewController!
}

// SwiftUI View
struct SwiftUIView: View {
    @StateObject private var coordinator: BridgingCoordinator

    init() {
        let coordinator = BridgingCoordinator()
        self._coordinator = StateObject(wrappedValue: coordinator)
    }

    var body: some View {
        VStack {
            Text("Swift UI View")

            Button(action: buttonTapped) {
                Text("Call function on UIViewControllerRepresentable VC")
            }
            .disabled(coordinator.vc == nil)

            UIViewControllerRepresentation(bridgingCoordinator: coordinator)
        }
    }
    
    private func buttonTapped() {
        coordinator.vc.doSomething()
    }
}

// The UIViewControllerRepresentable of the ViewController
struct UIViewControllerRepresentation: UIViewControllerRepresentable {
    var bridgingCoordinator: BridgingCoordinator

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    func makeUIViewController(context: Context) -> some UIViewController {
        let vc = ViewController()
        vc.bridgingCoordinator = bridgingCoordinator
        return vc
    }

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

    class Coordinator: NSObject {
        let parent: UIViewControllerRepresentation
        init(_ view: UIViewControllerRepresentation) {
            self.parent = view
        }
    }
}

// ViewController which contains functions that need to be called from SwiftUI
class ViewController: UIViewController {
    // The BridgingCoordinator received from the SwiftUI View
    var bridgingCoordinator: BridgingCoordinator!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Set self to the BridgingCoordinator
        bridgingCoordinator.vc = self
    }

    func doSomething() {
        print("Received function call from SwiftUI View")
    }
}
Quarters answered 25/1, 2022 at 10:18 Comment(1)
Seems like this is still the best way to do it in 2023.Lightly
D
3

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
}
Dnieper answered 21/6, 2020 at 22:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.