2

I am building a camera app with all the UI in SwiftUI (parent) holding a UIKit Controller that contains all the recording functionalities. The UI is pretty complex, so would like if possible to remain with this structure for the project.

The UIKit Class has some functions like startRecord() stopRecord() which I would like to be triggered from the SwiftUI view. For that reason, I would like to 'call' the UIKit functions from my SwiftUI view.

I am experimenting with UIViewControllerRepresentable, being able to perform updates on a global variable change, but I am still not able to call the individual functions I want to trigger from the SwiftUI parent.

Here its the SwiftUI file:

init(metalView: MetalViewController?) {
    self.metalView = MetalViewController(appStatus: appStatus)
}

var body: some View {
    
    ZStack {
        
        // - Camera view
        metalView
            .edgesIgnoringSafeArea(.top)
            .padding(.bottom, 54)
        
        VStack {
            
            
            LateralMenuView(appStatus: appStatus, filterTooltipShowing: $_filterTooltipShowing)
            
            Button("RECORD", action: {
                print("record button pressed")
                metalView?.myMetalDelegate.switchRecording(). // <-- Not sure about this
            })

Here is the MetalViewController:

protocol MetalViewControllerDelegate {
    func switchRecording()
}

// MARK: - The secret sauce for loading the MetalView (UIKit -> SwiftUI)
struct MetalViewController: UIViewControllerRepresentable {

var appStatus: AppStatus
typealias UIViewControllerType = MetalController
var myMetalDelegate: MetalViewControllerDelegate!

func makeCoordinator() -> Coordinator {
    Coordinator(metalViewController: self)
}

func makeUIViewController(context: UIViewControllerRepresentableContext<MetalViewController>) -> MetalController {
    let controller = MetalController(appStatus: appStatus)
    return controller
}

func updateUIViewController(_ controller: MetalController, context: UIViewControllerRepresentableContext<MetalViewController>) {
    controller.changeFilter()
}

class Coordinator: NSObject, MetalViewControllerDelegate {
    var controller: MetalViewController

    init(metalViewController: MetalViewController) {
        controller = metalViewController
    }
    func switchRecording() {
        print("just testing")
    }
}

}

and the UIKit Controller...

class MetalController: UIViewController {

var _mydelegate: MetalViewControllerDelegate?
...
override func viewDidLoad() {
 ...
    self._mydelegate = self
}

extension MetalController: MetalViewControllerDelegate {
    func switchRecording() {
        print("THIS SHOULD BE WORKING, BUT ITS NOT")
    }
}

1 Answer 1

3

I like to use Combine to pass messages through an ObservableObject to the UIKit views. That way, I can call them imperatively. Rather than trying to parse your code, I made a little example of the concept:

import SwiftUI
import Combine

enum MessageBridgeMessage {
    case myMessage(parameter: Int)
}

class MessageBridge : ObservableObject {
    @Published var result = 0
    
    var messagePassthrough = PassthroughSubject<MessageBridgeMessage, Never>()
}

struct ContentView : View {
    @StateObject private var messageBridge = MessageBridge()
    
    var body: some View {
        VStack {
            Text("Result: \(messageBridge.result)")
            Button("Add 2") {
                messageBridge.messagePassthrough.send(.myMessage(parameter: messageBridge.result))
            }
            VCRepresented(messageBridge: messageBridge)
        }
    }
}

struct VCRepresented : UIViewControllerRepresentable {
    var messageBridge : MessageBridge
    
    func makeUIViewController(context: Context) -> CustomVC {
        let vc = CustomVC()
        context.coordinator.connect(vc: vc, bridge: messageBridge)
        return vc
    }
    
    func updateUIViewController(_ uiViewController: CustomVC, context: Context) {
        
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    class Coordinator {
        private var cancellable : AnyCancellable?
        
        func connect(vc: CustomVC, bridge: MessageBridge) {
            cancellable = bridge.messagePassthrough.sink(receiveValue: { (message) in
                switch message {
                case .myMessage(let parameter):
                    bridge.result = vc.addTwo(input: parameter)
                }
            })
        }
    }
}

class CustomVC : UIViewController {
    func addTwo(input: Int) -> Int {
        return input + 2
    }
}

In the example, MessageBridge has a PassthroughSubject that can be subscribed to from the UIKit view (or in this case, UIViewController). It's owned by ContentView and passed by parameter to VCRepresented.

In VCRepresented, there's a method on the Coordinator to subscribe to the publisher (messagePassthrough) and act on the messages. You can pass parameters via the associated properties on the enum (MessageBridgeMessage). Return values can be stored on @Published properties on the MessageBridge if you need them (or, you could setup another publisher to go the opposite direction).

It's a little verbose, but seems to be a pretty solid pattern for communication to any level of the tree you need (SwiftUI view, representable view, UIKit view, etc).

Sign up to request clarification or add additional context in comments.

2 Comments

Impressive. How it could be done with protocols instead? Is the context.coordinator.connect what I was missing?
SwiftUI and Combine, because of their reliance on property wrappers, tend to not always work well with protocols. But you can certainly give it a shot and see what you can abstract.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.