|
import SwiftUI |
|
|
|
/// Extracts a value from the environment by the name of its associated EnvironmentKey. |
|
/// Can be used to grab private environment values such as foregroundColor ("ForegroundColorKey"). |
|
func extractEnvironmentValue<T>(env: EnvironmentValues, key: String, as: T.Type) -> T? { |
|
func keyFromTypeName(typeName: String) -> String? { |
|
let expectedPrefix = "TypedElement<EnvironmentPropertyKey<" |
|
guard typeName.hasPrefix(expectedPrefix) else { |
|
return nil |
|
} |
|
let rest = typeName.dropFirst(expectedPrefix.count) |
|
let expectedSuffix = ">>" |
|
guard rest.hasSuffix(expectedSuffix) else { |
|
return nil |
|
} |
|
let middle = rest.dropLast(expectedSuffix.count) |
|
return String(middle) |
|
} |
|
|
|
/// `environmentMember` has type (for example) `TypedElement<EnvironmentPropertyKey<ForegroundColorKey>>` |
|
/// TypedElement.value contains the value of the key. |
|
func extract(startingAt environmentNode: Any) -> T? { |
|
let mirror = Mirror(reflecting: environmentNode) |
|
|
|
let typeName = String(describing: type(of: environmentNode)) |
|
if let nodeKey = keyFromTypeName(typeName: typeName) { |
|
if key == nodeKey { |
|
// Found a match |
|
if let value = mirror.descendant("value", "some") { |
|
if let typedValue = value as? T { |
|
return typedValue |
|
} else { |
|
assertionFailure("Value for key '\(key)' in the environment is of type '\(type(of: value))', but we expected '\(String(describing: T.self))'.") |
|
} |
|
} else { |
|
assertionFailure("Found key '\(key)' in the environment, but it doesn't have the expected structure. The type hierarchy may have changed in your SwiftUI version.") |
|
} |
|
} |
|
} else { |
|
assertionFailure("Encountered type '\(typeName)' in environment, but expected 'TypedElement<EnvironmentPropertyKey<…>>'. The type hierarchy may have changed in your SwiftUI version.") |
|
} |
|
|
|
// Environment values are stored in a doubly linked list. The "before" and "after" keys point |
|
// to the next environment member. |
|
if let linkedListMirror = mirror.superclassMirror, |
|
let nextNode = linkedListMirror.descendant("after", "some") { |
|
return extract(startingAt: nextNode) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
let mirror = Mirror(reflecting: env) |
|
if let firstEnvironmentValue = mirror.descendant("plist", "elements", "some") { |
|
return extract(startingAt: firstEnvironmentValue) |
|
} else { |
|
return nil |
|
} |
|
} |
|
|
|
@propertyWrapper struct StringlyTypedEnvironment<Value> { |
|
final class Store<Value>: ObservableObject { |
|
var value: Value? = nil |
|
} |
|
|
|
@Environment(\.self) private var env |
|
// `wrappedValue.set` must be nonmutating, so we need some kind of external storage for the value. |
|
// Not sure this is the best way. I tried using `@State`, but that crashes. `@StateObject` is |
|
// convenient but we don't actually need Store to be an ObservableObject. 🤷♂️ |
|
@StateObject private var store: Store<Value> = Store() |
|
|
|
var key: String |
|
|
|
init(key: String) { |
|
self.key = key |
|
} |
|
|
|
private(set) var wrappedValue: Value? { |
|
get { store.value } |
|
nonmutating set { store.value = newValue } |
|
} |
|
} |
|
|
|
extension StringlyTypedEnvironment: DynamicProperty { |
|
func update() { |
|
wrappedValue = extractEnvironmentValue(env: env, key: key, as: Value.self) |
|
} |
|
} |
|
|
|
struct ChildView: View { |
|
@StringlyTypedEnvironment(key: "ForegroundColorKey") var fgColor: Color? |
|
|
|
var body: some View { |
|
Text("Hello") |
|
.foregroundColor(.red) |
|
.padding() |
|
.background(fgColor) |
|
} |
|
} |
|
|
|
struct ContentView: View { |
|
var body: some View { |
|
ChildView() |
|
.foregroundColor(Color.yellow) |
|
} |
|
} |
|
|
|
import PlaygroundSupport |
|
let view = ContentView() |
|
PlaygroundPage.current.setLiveView(view) |