简体   繁体   中英

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 @Environm.netObject 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?

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.

Here's my take on tackling this problem. My app targets iOS 14 or above:

The current state

I have a Main.storyboard file with one view controller scene set as the initial view controller with custom class ViewController . Here's the custom class implementation:

import UIKit

class ViewController: UIViewController {
    @IBOutlet var label: UILabel!
}

The goal

To use this class in a SwiftUI app life cycle and make it react and interact to @EnvironmentObject instance (In this case let's call it a theme manager).

Solution

I will define a ThemeManager observable object with a Theme published property like so:

import SwiftUI

class ThemeManager: ObservableObject {
    @Published var theme = Theme.purple
}

struct Theme {
    let labelColor: Color
}

extension Theme {
    static let purple = Theme(labelColor: .purple)
    static let green = Theme(labelColor: .green)
}

extension Theme: Equatable {}

Next, I created a ViewControllerRepresentation to be able to use the ViewController in SwiftUI:

import SwiftUI

struct ViewControllerRepresentation: UIViewControllerRepresentable {
    @EnvironmentObject var themeManager: ThemeManager

    // Use this function to pass the @EnvironmentObject to the view controller 
    // so that you can change its properties from inside the view controller scope.
    func makeUIViewController(context: Context) -> ViewController {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let viewController = storyboard.instantiateInitialViewController { coder in
            ViewController(themeManager: themeManager, coder: coder)
        }
        return viewController!
    }

    // Use this function to update the view controller when the @EnvironmentObject changes.
    // In this case I modify the label color based on the themeManager.
    func updateUIViewController(_ uiViewController: ViewController, context: Context) {
        uiViewController.label.textColor = UIColor(themeManager.theme.labelColor)
    }
}

I then updated ViewController to accept a themeManager instance:

import UIKit

class ViewController: UIViewController {
    @IBOutlet var label: UILabel!

    let themeManager: ThemeManager

    init?(themeManager: ThemeManager, coder: NSCoder) {
        self.themeManager = themeManager
        super.init(coder: coder)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @IBAction func toggleTheme(_ sender: UIButton) {
        if themeManager.theme == .purple {
            themeManager.theme = .green
        } else {
            themeManager.theme = .purple
        }
    }
}

Now, the last thing to do is create an instance of the theme manager and pass it as an environment object to the view controller representation:

import SwiftUI

@main
struct ThemeEnvironmentApp: App {
    @StateObject private var themeManager = ThemeManager()

    var body: some Scene {
        WindowGroup {
            ViewControllerRepresentation()
                .environmentObject(themeManager)
        }
    }
}

Running the app shows our view controller with a label and a button. Tapping the button triggers the IBAction , which changes the themeManager.theme , which triggers a call to the representation's updateUIViewController(_:, context:) :

解决方案演示

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM