How does the SwiftUI Environment work and can it be used outside SwiftUI for Dependency Injection?

Apologies for the lack of code syntax highlighting. I have plans to migrate my whole blog at some point in the future but I needed to get this out of my head before that. 

I woke up this morning with several questions in my head and I couldn't find an existing blog post that answered one. If one does exist let me know I'd love to compare notes.

  1. How does the SwiftUI Environment work?
  2. Can it be used outside SwiftUI for more general dependency injection?
  3. If not how similar can I make something to do the job?
  4. Why do people keep telling me the Environment is unsafe and will crash if the object requested hasn't been added?

By early evening I think I had the answers. By night the blog was written and by the early hours the Swift Package was published.

[Update 17th Feb 2021 - Follow up post now available with information about the Swift Package status.]

[Update 20th Feb 2021 - Package 1.0.0 released.]

Spoiler 1 - I Know Property Wrappers

I now have enough understanding of property wrappers to understand much but not all of what the SwiftUI environment is doing.

I didn't know Property Wrappers before this morning, beyond the concept and use of existing ones. I'm now much more familiar with them including the future extension from the Evolution proposal that seems to be live in Swift and it seems is the basis for the @Environment and @Published property wrappers.

Spoiler 2 - No it can't be used for DI but...

I don't believe it can be usefully used outside of SwiftUI however it really doesn't take many lines of code to replicate the useful parts for general usage

Spoiler 3 - I can replicate enough (using unnofficial API)

I have a practical injection approach set up where I can manually inject an an object into classes (not structs) and have a property wrapper make properties as available with the registration process just the same as for the environment. The framework is below and is a little over 30 lines of code for a typesafe dependency injection framework (unfortunately using an unsupported compiler feature).

Spoiler 4 - Environment is safe, EnvironmentObject is unsafe

I'm pretty sure they have a read a blog post about @EnvironmentObject which is different from @Environment and have confused the two. With @EnvironmentObject you get a crash if an object of the requested type hasn't been added. Using the Environment has much more boiler plate but I believe is completely robust and safe due to the way you declare the EnvironmentKey with a defaultValue so you can never fail to have a value.

If you think I've got it wrong please shout (applies to everything written here).

Why Can't the Environment be used for non-SwiftUI Dependency Injection

Slightly to my surprise you can add @Environment wrapped properties to a normal class or struct and they can successfully read the property and get the defaultValue for the key. What I couldn't find was any way to customise the contents of the environment. That rules them it out for any kind of dynamic injection framework that be customised/overridden as it gets passed through the object graph.

Micro Injection - A Tiny DI framework that won't hurt a bit

By using the (not-officially supported) property wrapper subscript to access the enclosing self it is possible to have an @Environment style property wrapper that takes a keypath argument. This gives completely typesafe access to the injected properties and you set up exactly the same way as you would when using SwiftUI environment variables. It is published here.

Usage

So to add an injectable value you do this:

struct BarKey: InjectionKey {
    static var defaultValue = Foo.shared // Could be constructed here or even a computed default
}

extension InjectionValues {
    var bar: Foo {
        get { self[BarKey.self] }
        set { self[BarKey.self] = newValue }
    }
}
        
    

Then you can do this to use the value:

class Whatever : Injectable {
    var injection: InjectedValues // Probably set in init but can be set after if you want.

    @Injection(\.bar) var myBar

    init(dInjection: InjectionValues) {
        self.injection = dInjection
       	...
        
    

The Entire Framework (about 40 lines plus comments/whitespace/closing braces)

This may be easier to read in the package repo.

public protocol InjectionKey {
   associatedtype Value
   static var defaultValue: Value { get }
}

extension InjectionKey {
    fileprivate static var dictKey: String {
        return String(reflecting: Self.self) // Unique name for the key type
    }
}

/// Like EnvironmentValues but this is to be manually passed through
/// your application and set as the `injection` property on any class
/// that you want to be able to use the `@Injection` property wrapper
/// on.
public struct InjectionValues {
    /// Create new empty environment
    public init() {}

    private var dict: [String: Any] = [:]

    /// You can call this directly but much nicer to extend
    /// InjectionValues and add a computed property to get/set it
    /// for you which is anyway required to get the `@Injection`
    /// property wrapper working
    open subscript<K>(key: K.Type) -> K.Value where K : InjectionKey {
        get { dict[K.dictKey].map { $0 as! K.Value} ?? K.defaultValue }
        set { dict[K.dictKey] = newValue }
    }
}

/// Conform to this  and you can then add `@Injection` wrapped
/// properties to your class
public protocol Injectable : class {
    /// This is where the `@Injection` properties will actually look
    /// up their values. You will often want to inject them
    var injection: InjectionValues { get }
}

/// The `@Injection` property wrapper can be added to classes
/// conforming to Injectable
/// and takes a keypath argument into InjectionValues (you should
/// extend InjectionValues with with properties that you want to be
/// able read from the Injection
@frozen @propertyWrapper public struct Injection<Value> {

    public let keyPath: KeyPath<InjectionValues, Value>
    @inlinable public init(_ key: KeyPath<InjectionValues, Value>) {
        keyPath = key
    }

    @inlinable public static subscript<OuterSelf : Injectable> (
        _enclosingInstance instance: OuterSelf,
        wrapped wrappedKeyPath: KeyPath<OuterSelf, Value>,
        storage storageKeyPath: KeyPath<OuterSelf, Self>
    ) -> Value {
        get {
            let keypath = instance[keyPath: storageKeyPath].keyPath
            return instance.injection[keyPath: keypath]
        }
    }

     @available(*, unavailable, message: "Expected subscript to be used")
     @inlinable public var wrappedValue: Value {
         get { fatalError() }
     }
}

InjectionValues - Class or Struct

I've made the InjectionValues a struct but it will need extra functionality adding so that in tests you can ensure only the injected values you expect the system under test to access are retreived.

How it works

The Container

The actual storage takes place in the [String: Any] dictionary inside InjectionValues . The dictionary keys internally are the full type names of the InjectionKey instances that the are defined like EnvironmentKey s are defined. To conform to the protocol they provide a default value which is returned if nothing is stored in the dictionary which also defines the type of the stored value. The subscript is how you access the values so it can ensure that everything is cast correctly to store and retreive because the arguments are the key types.

extension InjectionKey {
    fileprivate static var dictKey: String {
        return String(reflecting: Self.self) // Unique name for the key type
    }
}

public struct InjectionValues {
    public init() {}

    private var dict: [String: Any] = [:]

    public subscript<K>(key: K.Type) -> K.Value where K : InjectionKey {
        get { dict[K.dictKey].map { $0 as! K.Value} ?? K.defaultValue }
        set { dict[K.dictKey] = newValue }
    }
}

public protocol InjectionKey {
    associatedtype Value
    static var defaultValue: Value { get }
}

With this in place we actually have a usable dependency injection system we just need to directly call the properties on the injection everywhere rather have the property wrapper bring them onto self.

struct BarKey: InjectionKey {
    static var defaultValue = Foo.shared // Could be constructed here or even a computed default
}

extension InjectionValues {
    var bar: Foo {
        get { self[BarKey.self] }
        set { self[BarKey.self] = newValue }
    }
}

class Thing {
    var injection = InjectionValues() // Could also be injected in the init or even set afterwards if  order of creation is tricky.
    
    init() {
        print(injection.bar) // Will print the default value (Foo.shared)
        injection.bar = Foo()
        print(injection.bar) // Will print the newly created Foo()
    }
}

The Property Wrapper

The property wrapper is the syntactic sugar over the usage of the Injection (although at the cost of using Swift that isn't officially supported. It also makes a clear declaration in the class of the injected properties that are used. So class Thing as seen above will become:

class Thing : Injectable {
    var injection = InjectionValues()
    @Injection(\.bar) var bar
    
    init() {
        print(bar) // Will print the default value (Foo.shared)
        injection.bar = Foo()
        print(bar) // Will print the newly created Foo()
    }
}

The framework's property wrapper itself is a bit ugly because it has to use the mechanism to access enclosing self that the property wrapper is used in to reach the injection property. Some of this like the frozen and inlinable are cargo culted from the Environment property wrapper declaration (the inlinable then requires the keypath to be public.

Note the _ on the enclosableInstance argument to the subscript marking this as unoffical Swift syntax. This is a bit of weird setup but creating the wrappedValue and making it unavailable is the mechanism to indicate the subscripted approach to the property wrapper will be taken.

As I only wanted a read only access to the injected values by this route to make accidental changes to the injection less likely it didn't work with ReferenceWritableKeyPath s for the wrapped and storage arguments. I did think that would mean that the property wrappers could be used on structs but that did not seem to be the case.

public protocol Injectable : class {
    var injection: InjectionValues { get }
}

@frozen @propertyWrapper public struct Injection<Value> {

    public let keyPath: KeyPath<InjectionValues, Value>
    @inlinable public init(_ key: KeyPath<InjectionValues, Value>) {
        keyPath = key
    }

    @inlinable public static subscript<OuterSelf : Injectable> (
        _enclosingInstance instance: OuterSelf,
        wrapped wrappedKeyPath: KeyPath<OuterSelf, Value>,
        storage storageKeyPath: KeyPath<OuterSelf, Self>
    ) -> Value {
        get {
            let keypath = instance[keyPath: storageKeyPath].keyPath
            return instance.injection[keyPath: keypath]
        }
    }

    @available(*, unavailable, message: "Expected subscript to be used")
    @inlinable public var wrappedValue: Value {
        get { fatalError() }
    }
}
        
    

Limitations

  1. This can't currently be used on structs.
  2. I'm not clear on the exact reason why not given that the in this case the property wrapper provides read only access. I understand the explanation that to be able to write through the wrapper meaningfully it needs to be reaching into an instance or it won't really take effect but with the read only case this isn't as clear to me.

  3. The dependency injection object needs to be manually passed in
  4. SwiftUI runtime I think keeps an object representing the necessary stuff the view needs that lives beyond the creation cycle (e.g. @State properties) and I think where in a View the @Environment property does magic to obtain that object and retreive the environment values from there. I'm yet to learn if there is some officially unsupported magic that could be replicating this.

    Also this object is fully exposed to direct access or modification if you wanted.

  5. This is unsupported Swift
  6. It could be broken by any release (although it is likely to be replaced with slightly different syntax to achieve the same thing. [Edit to add: If the feature disappears I think that the code could be fixed up to replace the @Injection properties with computed vars using a regular expression that would probably just need the correct types applying to them manually]. Alternatively that approach could be used from the start and the property wrapper ignored].

  7. I haven't used this in anger yet
  8. Only tested in a very basic dummy project but it seems to work.

Additional Sources

Swift Evolution - Accepted Property Wrappers Proposal (enclosing self future extension possibility)

 Swift By Sundell article on the topic (showing how @Published can be implemented): 

Thanks to ⁷⁄₂ kostki on the Vapor Discord who pointed out the underscore when I was trying to find whether the subscript based property wrapper had properly been through Swift Evolution.

Checkout My Latest App - Fast Lists Sync

A complete rebuild of my original checklists app in SwiftUI with iCloud Sync, drag and drop, dark mode and multi-window support: App Store or more information