Home Swift Dynamic Member Lookup
Post
Cancel

Swift Dynamic Member Lookup

Since Swift is statically typed, Coder Orangu 🦧 missed out on some of the runtime flexibility offered by Objective-C. He was working on a setting screen of an applciation and it has a large number of fields. He was tired of declaring each property of the settings. He wants a dynamic way of adding property into a struct type.

He was lucky that Apple introduced the DynamicMemberLookup feature with Swift 5.0. He was eager to implement the feature within the applciation.

Basics by Coder Orangu 🦧

Any custom type (class/struct/enum) can support the feature. It should be annotated with the @dynamicMemberLookup attribute and contain a method subscript.

1
2
3
4
5
6
@dynamicMemberLookup
struct Settings {
    subscript(dynamicMember key: String) -> Any? {
        return UserDefaults.standard.value(forKey: key)
    }
}

As Settings type is marked with @dynamicMemberLookup attribute. It can return any parameter you ask.

1
2
let appSettings = Settings()
print(self.appSettings.theme)

But wait, how to set a value? Let’s try to set a value.

1
2
let appSettings = Settings()
self.appSettings.theme = "Dark"

Error: Cannot assign through dynamic lookup property: ‘self’ is immutable

To remove the error a small update is required in the subscript method so that it supports the setter also.

Mutable Dynamic Lookup

1
2
3
4
5
6
7
8
9
10
@dynamicMemberLookup
struct Settings {
    subscript(dynamicMember key: String) -> Any? {
        get {
            return UserDefaults.standard.value(forKey: key)
        } set {
            UserDefaults.standard.setValue(newValue, forKey: key)
        }
    }
}

Is it safe?

Coder Orangu is worried that Swift’s reputation as a safe language is compromised here, as you can access any property on Settings at runtime, even if that may not exist.

Coder Orangu tried accessing a property which never set on the appSettings:

1
2
let appSettings = Settings()
print(self.appSettings.font)

It simply prints nil as expected. It’s the Coder’s responsibility here to safely typecast to an expected type and then use that.

The only safety we can ensure here is return type of the subscript method should not be generic which can lead to unexpected results while using them.

For instance, if the return type was AnyObject rather than Any?, you’d have access to its methods, bypassing Swift’s type safety protections. So here safety is totally in your hands.

1
2
3
4
5
6
7
8
@dynamicMemberLookup
struct Settings {
    subscript(dynamicMember key: String) -> AnyObject {
        get {
            return UserDefaults.standard.value(forKey: key) as AnyObject
        }
   }
}
1
2
//Crash 
self.appSettings.font.setTitle(<#T##title: String?##String?#>, for: <#T##UIControl.State#>)

Uses of Dynamic Member Lookup by Coder Orangu 🦧

Coder Orangu is confused about the daily necessity of this feature. It’s prone to bugs, reduces code readability, and overrides Swift’s safety. Nevertheless, here are some potential use cases:

  1. Data Parsing: When dealing with backend data containing numerous attributes, define a Parsed object using Dynamic member lookup to simplify the parsing.
  2. Data Storage: As given in the example to access/store any property in the User Default.
  3. Adding an extension on existing Component: You can use the feature with Keypath to expose the attributes of an object in any third-party library.

For example, create an extension to add a 3D effect to View. Suppose you have a login view and want to add a 3D effect:

Login View:

1
2
3
4
5
6
7
struct LoginView: View {
    var username: String
    
    var body: some View {
        Text("User")
    }
}

3D Effect Extension on LoginView with all the attributes exposed of the View:

1
2
3
4
5
6
7
8
9
10
@dynamicMemberLookup
struct ThreeDView {
    public var shadowDepth: Float
    public var angle: Float
    private(set) var _view: LoginView
    
    subscript<T>(dynamicMember keyPath: KeyPath<LoginView, T>) -> T {
        return _view[keyPath: keyPath]
    }
}

Here in subscript method, the KeyPath of LoginView type has been used, so all the properties applicable to LoginView can be directly accessed via ThreeDView.

You can directly use the username on ThreeDView:

1
2
3
let loginView = LoginView(username: "Code World")
let threeDView = ThreeDView(shadowDepth: 4.0, angle: 125.0, _view: loginView)
print(threeDView.username)

This is all about today’s topic. Coder Orangu is waiting 🦧⏲️ for the feedback.

This post is licensed under CC BY 4.0 by the author.

Property Wrappers in Swift - (Must Use Feature)

-

Comments powered by Disqus.