swiftswiftuiuikitenvironmentobject

How do I pass an EnvironmentObject from UIKit view controller to a SwiftUI view


I have a SwiftUI project that uses EnvironmentObject to update a label accordingly. In the same project, I have a UIViewController, that I am hosting in my SwiftUI view by conforming that view controller to the UIViewControllerRepresentable protocol. I want to be able to control the label in my SwiftUI view from my UIKit code. Is there a way I can pass the EnvironmentObject from my UIKit code? If not, what would be the ideal way to pass data from my UIKit based view to the SwiftUI view.

Please note, I have simplified the project for the sake of posting the question here. Here is the code:

MyApp.swift

import SwiftUI

@MainActor class Options: ObservableObject {
    @Published var option = 1
}

@main
struct MyApp: App {
    
    @StateObject var options = Options()
    
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(options)
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView: View {
    
    @EnvironmentObject var options: Options
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            
            if options.option == 0 {
                Text("Hello, world!")
            } else {
                Text("Other Options")
            }
            
            MyUIView()
        }
        .padding()
    }
}

#Preview {
    ContentView().environmentObject(Options())
}

MyUIViewController.swift

import UIKit

class MyUIViewController: UIViewController {
    
    var btn: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        
        var config = UIButton.Configuration.filled()
        config.title = "Change Option"
        
        btn = UIButton(configuration: config)
        btn.addAction(UIAction(handler: { UIAction in
            /* Here, I need to change the option 
             and pass it to update the label in my SwiftUI view. */
        }), for: .touchUpInside);
        
        btn.frame = CGRect(x: 0, y: 0, width: 100, height: 50)
        
        self.view.addSubview(btn)
    }
}

MyUIView.swift

import SwiftUI

struct MyUIView: UIViewControllerRepresentable {
    
    func makeUIViewController(context: Context) -> UIViewController {
       
        return  MyUIViewController()
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        
    }
}

Solution

  • Naturally, you should add a Options property in your view controller:

    class MyUIViewController: UIViewController {
        
        var btn: UIButton!
        
        var options: Options?
        
        override func loadView() { // loadView is where you should set up your views
            view = UIView()
            var config = UIButton.Configuration.filled()
            config.title = "Change Option"
            
            btn = UIButton(configuration: config)
            btn.addAction(UIAction(handler: { UIAction in
                // set the published property here
                self.options?.option = 0
            }), for: .touchUpInside);
            
            btn.frame = CGRect(x: 0, y: 0, width: 100, height: 50)
            
            view.addSubview(btn)
        }
    }
    

    Then in the UIViewControllerRepresentable, get the EnvironmentObject the same way as usual. Then you can assign it to the options property in the view controller:

    struct MyUIView: UIViewControllerRepresentable {
        
        @EnvironmentObject var options: Options
        
        func makeUIViewController(context: Context) -> MyUIViewController {
            return  MyUIViewController()
        }
        
        func updateUIViewController(_ uiViewController: MyUIViewController, context: Context) {
            uiViewController.options = options
        }
    }