Search code examples
iosswiftswiftuiuikit

How to set @EnvironmentObject in an existing Storyboard based project?


I have an iOS project built with Storyboard and UIKit. Now I want to develop the new screens using SwiftUI. I added a Hosting View Controller to the existing Storyboard and used it to show my newly created SwiftUI view.

But I couldn't figure out how to create an @EnvironmenetObject that can be used anywhere throughout the application. I should be able to access/set it in any of my UIKit based ViewController as well as my SwiftUI views.

Is this possible? If so how to do it? In a pure SwiftUI app, we set the environment object like below,

@main
struct myApp: App {
    @StateObject var item = Item()

    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(item)
        }
    }
}

But in my case, there is no function like this since it is an existing iOS project with AppDelegate and SceneDelegate. And the initial view controller is marked in Storyboard. How to set this and access the object anywhere in the app?


Solution

  • The .environmentObject modifier changes the type of the view from ItemDetailView to something else. Force casting it will cause an error. Instead, try wrapping it into an AnyView.

    class OrderObservable: ObservableObject {
        
        @Published var order: String = "Hello"
    }
    
    struct ItemDetailView: View {
        
        @EnvironmentObject var orderObservable: OrderObservable
        
        var body: some View {
            
            EmptyView()
                .onAppear(perform: {
                    print(orderObservable.order)
                })
        }
    }
    
    class ItemDetailViewHostingController: UIHostingController<AnyView> {
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
    
        required init?(coder: NSCoder) {
            super.init(coder: coder,rootView: AnyView(ItemDetailView().environmentObject(OrderObservable())))
        }
    }
    

    This works for me. Is this what you require?

    EDIT: Ok, so I gave the setting the property from a ViewController all through the View. It wasn't as easy as using a property wrapper or a view modifier, but it works. I gave it a spin. Please let me know if this satisfies your requirement. Also, I had to get rid of the HostingController subclass.

    class ViewController: UIViewController {
        
        var orderObservable = OrderObservable()
        
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            guard let myVC = (segue.destination as? MyViewController) else { return }
            myVC.orderObservable = orderObservable
        }
    }
    
    class MyViewController: UIViewController {
        
        var orderObservable: OrderObservable!
        var anycancellables = Set<AnyCancellable>()
        @IBAction @objc func buttonSegueToHostingVC() {
            let detailView = ItemDetailView().environmentObject(orderObservable)
            present(UIHostingController(rootView: detailView), animated: true)
            
            orderObservable.$order.sink { newVal in
                print(newVal)
            }
            .store(in: &anycancellables)
        }
    }
    
    
    class OrderObservable: ObservableObject {
        
        @Published var order: String = "Hello"
        
        init() {
            DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
                self.order = "World"
            }
        }
    }
    
    struct ItemDetailView: View {
        
        @EnvironmentObject var orderObservable: OrderObservable
        
        var body: some View {
            
            Text("\(orderObservable.order)")
        }
    }
    

    Basically I'm creating the observable object in the ViewController class, passing it to the MyViewController class and finally create a hosting controller with the ItemDetailView and setting it's environmentObject and presenting it.