Articles, podcasts and news about Swift development, by John Sundell.

Combining dynamic member lookup with key paths

Published on 10 Apr 2020

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
    }
}