Skip to content

Conversation

@MiaKoring
Copy link
Contributor

This PR adds:

  • EnvironmentValues/with(key:, value:) //equivalent to with(keyPath: newValue:) but for non-builtin values
  • view modifiers for both builtin (key path) and not builtin (EnvironmentKey) values

For demonstration and testing purposes I changed GreetingGeneratorExample’s latest greeting display to get its value over an environment key.

The @Environment property wrapper was changed to additionally support EnvironmentKeys.

I did some limited testing which worked like expected. The modifiers just use the EnvironmentModifier View just like we do with most other modifiers.

Copy link
Owner

@stackotter stackotter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for these changes! They will make working with custom environment values much easier.

While reviewing the PR, I googled stuff about SwiftUI's environment modifiers, and I found that SwiftUI works around the distinction between custom keys and builtin environment properties (keypaths) by instructing users to extend EnvironmentValues with their own custom properties that utilise the environment key. Your implementation doesn't stop users from taking that approach, but I'm wondering whether we should try to push users towards the SwiftUI approach, or instead let them choose.

I was leaning towards letting users choose because it makes defining custom environment values very simple (just define the key). However, I realised that having these two different types of environment properties makes it tricky for libraries to provide methods that are generic over environment properties, because they would need to vend two separate (but very similar) methods for each abstraction.

For that reason, I think that it's probably best to not have the environment key based methods? But I'm open to discussion on that.

}

struct EnvironmentDisplay: View {
@Environment(TestKey.self) var value: String?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a newline after this.

}
}

struct EnvironmentDisplay: View {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a doc comment along the lines of /// This intermediate view exists to show the usage of custom environment keys. In reality it is not necessary. so that future readers don't get too confused.

}
}

struct TestKey: EnvironmentKey {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to LatestGreetingKey to make its usage more self-evident.

}
}

struct EnvironmentDisplay: View {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to LatestGreetingDisplay for clarity.

Comment on lines +39 to +40
var keyPath: KeyPath<EnvironmentValues, Value>?
var environmentKey: (any EnvironmentKey.Type)?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduce an enum to encode the requirement that we want one property or the other to be non-nil (not neither, and not both). This enum would just be an internal detail.

enum EnvironmentLocation<Value> {
    case keyPath(KeyPath<EnvironmentValues, Value>)
    case environmentKey(any EnvironmentKey.Type)
}

You can then replace these two properties with a single var location: EnvironmentLocation<Value> property, and then replace the existing if lets with switch statements.

if let keyPath {
value.value = environment[keyPath: keyPath]
} else if let environmentKey {
value.value = (environment[environmentKey] as! Value)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought about how to avoid this force unwrap a little bit, and it seems like we can make Value a primary associated type of EnvironmentKey, and then update the environmentKey property to have type any EnvironmentKey<Value>.Type. This should make things a bit safer, and appears to be a backwards compatible change (because users of EnvironmentKey aren't forced to use the primary associated type syntax)?

I had to Google the syntax, so I figured I might as well include it here. To make Value a primary associatetype do the following;

protocol EnvironmentKey<Value> {
    associatedtype Value
    ...
}


/// Returns a copy of the environment with the specified key set to the
/// provided new value.
public func with<T: EnvironmentKey>(key: T.Type, value: T.Value) -> Self {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give the parameters each the underscore label to match the keypath variant (and rename value to newValue)


extension View {
/// Modifies the environment of the View its applied to.
public func environment<T: EnvironmentKey>(key: T.Type, value: T.Value) -> some View {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the parameter labels to match the following method, and rename value to newValue

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants