Skip to content

Instantly share code, notes, and snippets.

@anandabits
Last active November 9, 2023 20:44
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save anandabits/f3c30916ff46c2fe9d4c73dabccff696 to your computer and use it in GitHub Desktop.
Save anandabits/f3c30916ff46c2fe9d4c73dabccff696 to your computer and use it in GitHub Desktop.
Towards a safer SwiftUI environment

Towards a safer SwiftUI environment

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.

Tuple intersection types

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)

Intersection difference

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

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...
}

Labeled variadic values

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")

Environment typealias

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.

Injection

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.

Hosting

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...)
}
@lunalugo25
Copy link

lunalugo25 commented May 15, 2020

In

typealias Three = (prop: Protocol1)
typealias Four = (prop: Protocol2)
typealias SecondPair = One & Two // (prop: Protocol1 & Protocol2)

The SecondPair should be:

typealias SecondPair = Three & Four // (prop: Protocol1 & Protocol2)

@anandabits
Copy link
Author

Thanks, updated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment