Skip to content

Instantly share code, notes, and snippets.

@ole
Created August 7, 2020 18:51
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ole/7d18e9e4b656f8cc1f24c7284b493df8 to your computer and use it in GitHub Desktop.
Save ole/7d18e9e4b656f8cc1f24c7284b493df8 to your computer and use it in GitHub Desktop.
Extract a value from the SwiftUI environment by the name of its associated EnvironmentKey (e.g. "ForegroundColorKey").

Extract a value from the SwiftUI environment by the name of its associated EnvironmentKey (e.g. "ForegroundColorKey").

Tested only in Xcode 12.0 beta 4 in the iOS simulator. May break in other environments because it uses reflection. If SwiftUI's private type hierarchy changes, it will probably stop working.

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment