Search code examples
swiftswiftui

How can I create an Environment without a defaultValue so it throws when used without being provided


I'm trying to require thing being supplied instead of providing it as a default with defaultValue = Thing(). If I make the environment optional with Thing?, then I have to unwrap it in each View. Without such an approach my initializer always appears to be called twice when I need to pass in a specific instance.

I'm trying with the code below, but it throws the fatal "Thing not provided" as soon as it runs. I'm providing the environment in thingApp, though, so I'm confused why this is happening. It happens even when @Environment(\.thing) var thing isn't used.

import SwiftUI

@main
struct thingApp: App {
    var thing = Thing()

    var body: some Scene {
        WindowGroup {
            ContentView().environment(\.thing, thing)
        }
    }
}

struct ContentView: View {
    @Environment(\.thing) var thing

    var body: some View {
        Text("count: \(self.thing.count)")
    }
}

@MainActor
@Observable class Thing {
    var count: Int = 3

    func inc() {
        self.count = self.count + 1
    }
}

private struct ThingKey: EnvironmentKey {
    static var defaultValue: Thing {
        fatalError("Thing not provided")
    }
}

extension EnvironmentValues {
    var thing: Thing {
        get { self[ThingKey.self] }
        set { self[ThingKey.self] = newValue }
    }
}

Solution

  • As ITGuy's answer says, you should use the other initialiser of Environment if the environment value is @Observable. However, this wouldn't work if you want to have multiple keys of the same type, or if your environment value is a value type.

    Another way is to create your own version of Environment that fatalErrors when the environment is not set.

    @propertyWrapper
    struct ForcedEnvironment<Value>: DynamicProperty {
        @Environment private var env: Value?
        
        init(_ keyPath: KeyPath<EnvironmentValues, Value?>) {
            _env = Environment(keyPath)
        }
        
        var wrappedValue: Value {
            if let env {
                return env
            } else {
                fatalError("\(Value.self) not provided")
            }
        }
    }
    
    extension EnvironmentValues {
        @Entry var thing: Thing? = nil
    }
    

    This takes a key path to an optional environment value. nil is treated as "not set", so remember to change the EnvironmentValues extension accordingly.

    Note that this only fatalErrors when the wrappedValue getter is called.

    Usage:

    struct ContentView: View {
        @ForcedEnvironment(\.thing) var thing
    
        var body: some View {
            Text("count: \(self.thing.count)")
        }
    }