Swift UI's @EnvironmentObject
is currently prone to runtime errors. If a view uses an environment object of a type that was not injected using environmentObject
a fatalError
occurs. This document sketches a design that would support compile-time checking of environment object usage. It requires a few language enhancements.
Swift's protocol composition types such as Protocol1 & Protocol2
are a form of intersection types. Other forms of intersection types are possible. For example, Flow has object intersection types. Tuple intersection types for Swift would be very similar to these.
Below, you can see the examples in the Flow documentation converted to Swift's tuples:
typealias One = (foo: Double)
typealias Two = (bar: Bool)
typealias Both = One & Two // (foo: Double, bar: Bool)
let both: Both = (foo: 42, bar: true)
Like Flow, when more than one tuple type in the intersection has the same label and all the types are compatible for composition via intersection (i.e. all protocol existentials (or classes) or all tuples) the intersection is applied. Continuing the adaptation of the Flow examples:
typealias One = (prop: Protocol1)
typealias Two = (prop: Protocol2)
typealias Both = One & Two // (prop: Protocol1 & Protocol2)
let both: Both = instanceOfTypeConformingToProtocol1AndProtocol2
We can combine the above examples into a more advanced example
typealias One = (foo: Double)
typealias Two = (bar: Bool)
typealias FirstPair = One & Two // (foo: Double, bar: Bool)
typealias Three = (prop: Protocol1)
typealias Four = (prop: Protocol2)
typealias SecondPair = Three & Four // (prop: Protocol1 & Protocol2)
typealias All = FirstPair & SecondPair // (foo: Double, bar: Bool, prop: Protocol1 & Protocol2)
It is allso possible to take the difference of intersections. Using the types defined above:
typealias AlsoFirstPair = All - Three - Four
typealias AlsoSecondPair = All - FirstPair
Property wrapper packs are a form of pack, similar to packs used in the pitched variadic generics and explicit memberwise initiailizers. A property wrapper pack is a type pack containing the names and base types of all properties that have the property wrapper applied to them. A property wrapper pack may be expanded into a tuple or tuple intersection using the @WrapperType...
syntax. For example:
struct MyView: View {
@EnvironmentObject var foo: FooObject
@EnvironmentObject var bar: BarObject
// resolves to the tuple type (foo: FooObject, bar: BarObject)
typealias EnvironmentObjects = @EnvironmentObject...
}
The variadic generics pitch introduces the notion of a variadic value which is similar to (but is not) a tuple. This notion can be extended by capturing @variadic
external labels as labels for the individual elements of the variadic value.
func foo<variadic T>(@variadic values: T) {}
// in this call T is resolved as a the labeled variadic `(first: Int, second: String)`
foo(first: 42, second: "hello")
The basis of the design is to add an Environment
typealias to the View
protocol as follows:
extension View {
typealias Environment = Body.Environment & @EnvironmentObject...
}
SwiftUI's primitive views have a body type of Never
(which also has a body type of Never
). Never
does not have any environment objects and neither do the views. Therefore this typealias resoves to Void
for primitive views.
The Environment
typealias is used to capture type information about the specific environment objects in use in a view hierarchy and flow it upwards.
As in SwiftUI's current design, the environmentObject
method is used to inject a dependency. This method now returns a new primitive EnvironmentObjectProviderView
:
extension View {
func environmentObjects<variadic ProvidedObjects: ObjectBinding>(
@variadic providedObjects: ProvidedObjects
) -> EnvironmentObjectProviderView<ProvidedObjects:, Self> {
return .init(content: self, providedObjects: providedObjects)
}
}
struct EnvironmentObjectProviderView<variadic ProvidedObjects: ObjectBinding, Content: View>: View {
@EnvironmentObject (Content.Environment - ProvidedObjects)...
var body: Never { fatalError() }
let content: Content
let providedObjects: ProvidedObjects
}
As with other primitive views, because it's body is of type Never
the body's contribution to the resolution of the Environment
typealias for this view is Void
. However, unlike the other primitive views this primitive unpacks its type arguments into @EnvironmentObject
(s), while stripping the provided environment object from the upstream environment requirements.
With environmental requirements in hand, hosting controllers are now able to require that any remaining environment objects be injected during initialilzation.
class UIHostingController<Content>: UIViewController where Content: View {
// The remaining environment requirements are unpacked inline in the initializer signature
// When all environment requirements have already been fulfiled this unpacks to no arguments
init(rootView: Content, @variadic environment: Content.Environment...)
}
In
The
SecondPair
should be: