J. Doe
J. Doe

Reputation: 13043

Why/when are uninitialized non-optional values allowed in Swift for EnvironmentObjects?

In Swift, this crashes at runtime:

class EmptyData: BindableObject {
    let didChange = PassthroughSubject<EmptyData, Never>()
}

struct RandomView : View {
    @EnvironmentObject var emptyData: EmptyData
    @EnvironmentObject var emptyData2: EmptyData

    var body: some View {
        Text("Hello World!")
    }
}

and in the SceneDelegate.swift:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    let window = UIWindow(frame: UIScreen.main.bounds)
    // The emptyData variables are not initialized as seen below
    window.rootViewController = UIHostingController(rootView: RandomView())
    self.window = window
    window.makeKeyAndVisible()
}

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

Fixing the problem isn't that hard, but rather strange:

window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()))

So what's happening here? I pass EmptyData() and SwiftUI decides that both emptyData and emptyData2 should be initialized with the same object reference? I can pass also other environmentobjects that do not even exists as variables in the RandomView instance:

window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()).environmentObject(SomeData()))

And SwiftUI just happily run, although SomeData() isn't used anywhere in the instance of RandomView() and should trigger a compile time error in my opinion.

Why are uninitialized values permitted at compile time without initializing them when initializing the object and why are we free to pass environment instances without doing anything with them? Looks a bit like Javascript to me, I loved the strong static safe typing in Swift... I don't see right away why the member-wise initializer just generates an initializer which takes the environment variables as it's parameter.

Upvotes: 3

Views: 1198

Answers (2)

SMP
SMP

Reputation: 1669

What is @EnvironmentObject?

A linked View property that reads a BindableObject supplied by an ancestor

So, the environment prop can be supplied to children from the ancestor, not necessarily it should come from its immediate parent. With that, take a look at the below snippet, since RandomViewGrandParent injects the required Env objects into the environment, RandomViewParent doesn't have to do anything if the children of RandomViewParent needs same Env obj. RandomViewParent can just initiate view without passing the env obj again.

class EmptyData: BindableObject {
    let didChange = PassthroughSubject<EmptyData, Never>()
}

struct RandomViewGrandParent : View {
    var body: some View {
        RandomViewParent().environmentObject(EmptyData())
    }
}

struct RandomViewParent : View {
    @EnvironmentObject var emptyData: EmptyData
    @EnvironmentObject var emptyData2: EmptyData

    var body: some View {
        RandomView()
    }
}

struct RandomView : View {
    @EnvironmentObject var emptyData: EmptyData
    @EnvironmentObject var emptyData2: EmptyData

    var body: some View {
        Text("Hello World!")
    }
}

And to ans your another question -

I pass EmptyData() and SwiftUI decides that both emptyData and emptyData2 should be initialized with the same object reference?

That's because EnvironmentObject conforms to BindableObject and BindableObject's didChange is a Publisher, so I believe it thinks both emptyData and emptyData2 wants to subscribe to the same events/values hence uses the same ref for both.

Upvotes: 1

Martin R
Martin R

Reputation: 539815

The EnvironmentObject property delegate has an init() method taking no parameters, and that provides an implicit initialization for the wrapped properties

@EnvironmentObject var emptyData: EmptyData
@EnvironmentObject var emptyData2: EmptyData

(this is explained in the Modern Swift API Design video roughly at 28:10). So that is why these (non-optional) properties do not need an (explicit) initial value.

The documentation also states that EnvironmentObject is (emphasis added)

... a dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view whenever the bindable object changes.

You must set a model object on an ancestor view by calling its environmentObject(_:) method.

So this is how I understand it:

  • If a matching bindable object (in your case: an instance of EmptyData) is found in the environment of the current view or one of its ancestors then the properties are initialized to this object.
  • If no matching bindable object if found in an ancestor view then the program terminates with a runtime error.
  • Environment objects can be used in all, some, or none of the views in the view hierarchy. (See Data Flow Through SwiftUI at 29:20.) Therefore it is not an error to provide an environment object (in your case: an instance of SomeData) which is not used in RandomView.

Upvotes: 1

Related Questions