@dynamicMemberLookup lets a type answer dot syntax for members it does not declare, by routing
the access through a subscript(dynamicMember:). Use it to make a wrapper type read like the
type it wraps.
String-Based Lookup
The string form turns any member name into a subscript key at runtime.
@dynamicMemberLookup
struct Cache {
private var storage: [String: Data] = [:]
subscript(dynamicMember key: String) -> Data? {
storage[key]
}
}
var cache = Cache()
let profile = cache.profile // storage["profile"]
The cost is compile-time safety: every name compiles, and the result is whatever your subscript decides at runtime. Any misspelling still builds.
KeyPath-Based Lookup
The KeyPath form keeps the ergonomics and restores the compiler check. A store can expose its
private state read-only, member by member.
@dynamicMemberLookup
final class Store<State, Action>: ObservableObject {
@Published private var state: State
private let reduce: (State, Action) -> State
init(initialState state: State, reduce: @escaping (State, Action) -> State) {
self.state = state
self.reduce = reduce
}
subscript<T>(dynamicMember keyPath: KeyPath<State, T>) -> T {
state[keyPath: keyPath]
}
func send(_ action: Action) {
state = reduce(state, action)
}
}
Only key paths rooted in State compile, so a wrong member is a compiler error, not a runtime
surprise.
print(store.isLoading) // fine, State.isLoading
print(store.favorites) // compiler error: State has no favorites
The state stays private, so callers read through the store but can only mutate through
send(_:).