Slarkerino
Slarkerino

Reputation: 63

How to reference with SwiftUI EnvironmentObject

According to the Apple Official guide I'm guessing if each view just has one environmentObject then doing that would be fine. When there happened to be more than one, I'm not sure how the object are referenced.

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData
...
 ForEach(userData.landmarks) { landmark in
                    if !self.userData.showFavoritesOnly || landmark.isFavorite {
                        NavigationLink(
                            destination: LandmarkDetail(landmark: landmark)
                                .environmentObject(self.userData)

                        ) {
                            LandmarkRow(landmark: landmark)
                        }
                    }
                }

Above is the code in apple's official guide where in LandMarkDetail file

struct LandmarkDetail: View {
    @EnvironmentObject var userData: UserData

I am wondering how is the environmentObject in LandMarkList referenced to the environment object in LandmarkDetail. Are they binded? I added a second EnvironmentObject in the file but nothing happened

struct LandmarkDetail: View {
    @EnvironmentObject var userData: UserData
    @EnvironmentObject var testData: UserData

So how to reference to each of the userData and testData

Edit: I tried to add environment object in following file:

import SwiftUI

struct LandmarkList: View {
    @EnvironmentObject private var userData: UserData
    @EnvironmentObject private var testData: TestData
    var body: some View {
        NavigationView {
            List {
                Toggle(isOn: $userData.showFavoritesOnly) {
                    Text("Show Favorites Only")
                }
                Toggle(isOn: $testData.testbool) {
                                   Text("Test")
                }
...
struct LandmarksList_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in
            LandmarkList()
                .previewDevice(PreviewDevice(rawValue: deviceName))
                .previewDisplayName(deviceName)
        }
        .environmentObject(UserData())
        .environmentObject(TestData())
    }
}
final class TestData: ObservableObject {
    @Published var testbool = false
}

The preview crashed. Xcode could compile but immediately gives following error

Fatal error: No ObservableObject of type TestData found.
A View.environmentObject(_:) for TestData may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-30.4/Core/EnvironmentObject.swift, line 55
2019-10-12 19:07:46.565707+0800 Landmarks[13034:643289] Fatal error: No ObservableObject of type TestData found.
A View.environmentObject(_:) for TestData may be missing as an ancestor of this view.: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/Monoceros_Sim/Monoceros-30.4/Core/EnvironmentObject.swift, line 55

Upvotes: 2

Views: 3158

Answers (2)

Siddhant Gupta
Siddhant Gupta

Reputation: 156

Please understand how to use Environment Objects in swiftUI. My explanation is a bit aligned with general coding norms here.

What I think apple tried with environment objects is to create a singleton class. You create it just once and use it everywhere. But because swiftUI is declarative, every class is created in top down order in swiftUI and you cannot pass data freely. You are very much restricted.

So how to use your singleton class (Environment Objects) in swiftUI.

These are the steps:

  1. Create 1 single instance of the class you want to access everywhere, and inject it at top level of your code (Remember top down), so that every child view is able to access it.
    struct Demo: App {
    var demoController = demoController()
    var body: some Scene {
        WindowGroup {
            ContentView()
            .environmentObject(demoController)
        }
    }
    }
  1. Then access that single instance using @EnvironmentObject, in whichever view you want.
    struct someOtherView: View {
    @EnvironmentObject var demoController: demoController
    var body: some View {
       }
    }
  1. Last the class that you are making single instance of, should conform to @ObservableObject, and declare @Published for your member variables that you want o be modified from different views.
    class demoController : ObservableObject {
    @Published var someMember:Bool = false
    }

For a much better explanation (and if you have some time) check this video of Sean Allen - https://www.youtube.com/watch?v=b1oC7sLIgpI&ab_channel=SeanAllen and go to time 11:27:53

Also your code will compile, but will still crash now, and that has everything to do with previews. So just like step 1, make changes in your preview to avoid crash. (Make your var static to be used in static functions)

struct someOtherView_Previews: PreviewProvider {
    
    static var demoController = demoController()
    
    static var previews: some View {
        DrawingView()
        .environmentObject(demoController)
    }
}

Upvotes: 1

Chris
Chris

Reputation: 8091

don't forget to do this in Scenedelegate to bind your object:

window.rootViewController = UIHostingController(rootView: contentView.environmentObject(TestData()).environmentObject(UserData()))

Upvotes: 2

Related Questions