Combining dynamic member lookup with key paths
At first, Swift’s @dynamicMemberLookup
attribute might seem like an odd feature, given that it can be used to circumvent much of the type safety that Swift otherwise puts such a strong emphasis on.
Essentially, adding that attribute to a class or struct enables us to add support for accessing any property on that type — regardless of whether that property actually exists or not. For example, here’s a Settings
type that currently implements @dynamicMemberLookup
like this:
@dynamicMemberLookup
struct Settings {
var colorTheme = ColorTheme.modern
var itemPageSize = 25
var keepUserLoggedIn = true
subscript(dynamicMember member: String) -> Any? {
switch member {
case "colorTheme":
return colorTheme
case "itemPageSize":
return itemPageSize
case "keepUserLoggedIn":
return keepUserLoggedIn
default:
return nil
}
}
}
To learn more about subscripts in general, check out “The power of subscripts in Swift”.
Since the above type supports dynamic member lookup, we can use any arbitrary name when accessing one of its properties, and the compiler won’t give us any kind of warning or error when there’s no declared property matching that name:
let settings = Settings()
let theme = settings.colorTheme
let somethingUnknown = settings.somePropertyName
Again, that might seem like an odd feature for Swift to support, but it’s incredibly useful when writing bridging code between Swift and more dynamic languages — such as Ruby, Python, or JavaScript — or when writing other kinds of proxy-based code.
However, there is one more way to use @dynamicMemberLookup
that can also be incredibly useful even within completely static Swift code — and that’s to combine it with key paths.
As an example, let’s revisit the Reference
type from “Combining value and reference types in Swift” (which enables a value type to be passed as a reference), and add support for dynamically looking up one of its wrapped Value
type’s members — but this time using a KeyPath
, rather than a String
:
@dynamicMemberLookup
class Reference<Value> {
private(set) var value: Value
init(value: Value) {
self.value = value
}
subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
value[keyPath: keyPath]
}
}
Now this is really cool, because what the above enables us to do is to access any of our Value
type’s properties directly as if they were properties of our Reference
type itself — like this:
let reference = Reference(value: Settings())
let theme = reference.colorTheme
Since we implemented our Reference
type’s dynamicMember
subscript using a key path, we won’t be able to look up any arbitrary property name when using it, like we could when using strings.
We can even add a mutable version too, by creating a subscript overload that accepts a WritableKeyPath
, and by then implementing both a getter and a setter for it:
extension Reference {
subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
get { value[keyPath: keyPath] }
set { value[keyPath: keyPath] = newValue }
}
}
With the above in place, we can now directly mutate any Value
that’s wrapped using our Reference
type — just as if we were mutating the reference instance itself:
let reference = Reference(value: Settings())
reference.theme = .oldSchool
Finally, just like how we in the original article extracted all of the mutating APIs from Reference
into a new MutableReference
type — let’s do that here as well, to be able to limit in which parts of our code base that mutations can occur:
@dynamicMemberLookup
class Reference<Value> {
fileprivate(set) var value: Value
init(value: Value) {
self.value = value
}
subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
value[keyPath: keyPath]
}
}
class MutableReference<Value>: Reference<Value> {
subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
get { value[keyPath: keyPath] }
set { value[keyPath: keyPath] = newValue }
}
}
Using the above, we can now easily pass a value type as a reference, and both read and mutate its properties as if we were accessing the wrapped value directly — for example like this:
class ProfileViewModel {
private let user: User
private let settings: MutableReference<Settings>
init(user: User, settings: MutableReference<Settings>) {
self.user = user
self.settings = settings
}
func makeEmailAddressIcon() -> Icon {
// Reading Setting's 'colorTheme' property:
var icon = Icon.email
icon.useLightVersion = settings.colorTheme.isDark
return icon
}
func rememberMeSwitchToggled(to newValue: Bool) {
// Mutating Setting's 'keepUserLoggedIn' property:
settings.keepUserLoggedIn = newValue
}
}