5

I'm looking to call a function inside a UIKit UIViewController from a button managed by Swift UI

In my Swift UI View I have:

struct CameraView: View {
        var body: some View {
            cameraViewController = CameraViewController()
...

which I see creates two instances, one directly created just like calling any class, and the other created by the required makeUIViewController method needed for Swift UI to manage UIKit UIViewControllers.

However when I attached a function to a button in my Swift UI say, cameraViewController.takePhoto() The instance that is referenced is not the one displayed.

How can I obtain the specific instance that is displayed?

2 Answers 2

9

There are probably multiple solutions to this problem, but one way or another, you'll need to find a way to keep a reference to or communicate with the UIViewController. Because SwiftUI views themselves are pretty transient, you can't just store a reference in the view itself, because it could get recreated at any time.

Tools to use:

  • ObservableObject -- this will let you store data in a class instead of a struct and will make it easier to store references, connect data, etc

  • Coordinator -- in a UIViewRepresentable, you can use a Coordinator pattern which will allow you to store references to the UIViewController and communicate with it

  • Combine Publishers -- these are totally optional, but I've chosen to use them here since they're an easy way to move data around without too much boilerplate code.

import SwiftUI
import Combine

struct ContentView: View {
    @StateObject var vcLink = VCLink()
    var body: some View {
        VStack {
            VCRepresented(vcLink: vcLink)
            Button("Take photo") {
                vcLink.takePhoto()
            }
        }
    }
}

enum LinkAction {
    case takePhoto
}

class VCLink : ObservableObject {
    @Published var action : LinkAction?
    
    func takePhoto() {
        action = .takePhoto
    }
}

class CustomVC : UIViewController {
    func action(_ action : LinkAction) {
        print("\(action)")
    }
}

struct VCRepresented : UIViewControllerRepresentable {
    var vcLink : VCLink
    
    class Coordinator {
        var vcLink : VCLink? {
            didSet {
                cancelable = vcLink?.$action.sink(receiveValue: { (action) in
                    guard let action = action else {
                        return
                    }
                    self.viewController?.action(action)
                })
            }
        }
        var viewController : CustomVC?
        
        private var cancelable : AnyCancellable?
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    func makeUIViewController(context: Context) -> CustomVC {
        return CustomVC()
    }
    
    func updateUIViewController(_ uiViewController: CustomVC, context: Context) {
        context.coordinator.viewController = uiViewController
        context.coordinator.vcLink = vcLink
    }
}

What happens here:

  1. VCLink is an ObservableObject that I'm using as a go-between to communicate between views
  2. The ContentView has a reference to the VCLink -- when the button is pressed, the Publisher on VCLink communicates that to any subscribers
  3. When the VCRepresented is created/updated, I store a reference to the ViewController and the VCLink in its Coordinator
  4. The Coordinator takes the Publisher and in its sink method, performs an action on the stored ViewController. In this demo, I'm just printing the action. In your example, you'd want to trigger the photo itself.
Sign up to request clarification or add additional context in comments.

Comments

3

It's possible to make a reference from SwiftUI to your view controller if you need to call its functions directly and without unnecessary code:

class Reference<T: AnyObject> {
    weak var object: T?
}

class PlayerViewController: UIViewController {
    func resume() {
        print("resume")
    }
    
    func pause() {
        print("pause")
    }
}

struct PlayerView: UIViewControllerRepresentable {
    let reference: Reference<PlayerViewController>
    
    func makeUIViewController(context: Context) -> PlayerViewController {
        let controller = PlayerViewController()
        
        // Set controller to the reference
        reference.object = controller

        return controller
    }
    
    func updateUIViewController(_ viewController: PlayerViewController, context: Context) {
    }
}

struct ContentView: View {
    let reference = Reference<PlayerViewController>()
    
    var body: some View {
        Button("Resume") {
            reference.object?.resume()
        }
        Button("Pause") {
            reference.object?.pause()
        }
        PlayerView(reference: reference)
    }
}

2 Comments

Try Adding { reference.object = uiViewController} in updateUIController to have the latest reference of viewcontroller.
This is essentially the Coordinator pattern.

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.