When we use UIKit, we’ll write a lot of code like this:

let imageView = UIImageView(image: image)
imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
imageView.backgroundColor = .white
imageView.alpha = 0.5
Copy the code

But in SwiftUI:

Image(uiImage: myImage)
    .frame(width: 100, height: 100)
    .background(Color.white)
    .opacity(0.5)
Copy the code

Do not know whether your grade is the same as me, feel latter has aesthetic feeling more than former, more concise. The former needs to be constantly set up through the imageView object, which is old-fashioned. The chain style of the latter is smooth and fast, without the need for temporary variables to manipulate the object, in one go.

The common thinking

To implement a similar chain style, we need to manually modify the corresponding properties.

For example, if you have a class like this:

class Scene {
    var title: String?
    var backgroudColor: UIColor?
}
Copy the code

Scene To implement the chain, we need to add corresponding methods for these attributes:

// every time a property is modified, the corresponding method is also modified
extension Scene {
    func title(_ title: String) -> Scene {
        self.title = title
        return self
    }
    
    func backgroundColor(_ color: UIColor) -> Scene {
        self.backgroudColor = color
        return self}}Copy the code

By passing back self, the chain call is implemented:

Scene()
    .title("Scene")
    .backgroundColor(.yellow)
Copy the code

If the Scene property changes, then the corresponding setting method should also change. No problem with labor costs.

But if it’s Swift5.1 or above, there’s another option.

Dynamic Member Lookup

In Swift4.2, Dynamic Member Lookup was added to Swift. DynamicMemberLookup = dynamicMemberLookup = dynamicMemberLookup = dynamicMemberLookup = dynamicMemberLookup = dynamicMemberLookup = dynamicMemberLookup = dynamicMemberLookup = dynamicMemberLookup = dynamicMemberLookup If the accessed property does not exist, the implemented subscript(dynamicMember member: String) method is called, and the key is passed in as member.

For example, if the structure originally defined:

struct Persion {
    var info: [String: Any]}Copy the code

Add @dynamicMemberLookup:

@dynamicMemberLookup
struct Persion {
    var info: [String: Any]
    
    subscript(dynamicMember infoKey: String) -> Any? {
        get {
            return info[infoKey]
        }
        set {
            info[infoKey] = newValue
        }
    }
}
Copy the code

We can then access the contents of info as if we were accessing the properties of the Persion object directly:

var persion = Persion(info: [:])
persion.name = "Emilia"
print(persion.name)
Copy the code

Introduce user-defined “Dynamic Member Lookup” Types proposal this feature is designed to interact with Dynamic languages such as Python.

But why chained calls are involved is because in Swift5.1, this feature was updated.

Key Path Member Lookup

In Swift5.1, you can also use a key path as a medium for dynamic member queries in addition to strings.

Suppose we define Persion as follows:

struct Person {
    struct Info {
        var name: String
    }
    var info: Info
}
Copy the code

Add Key path Member lookup

@dynamicMemberLookup
struct Person {
    struct Info {
        var name: String
    }
    var info: Info
    
    subscript<Value> (dynamicMember keyPath: WritableKeyPath<Info.Value>) -> Value {
        get {
            return info[keyPath: keyPath]
        }
        set {
            info[keyPath: keyPath] = newValue
        }
    }
}
Copy the code

In addition to being set via persion.info.name, you can now:

// Syntax can be highlighted
var persion = Person(info: Person.Info(name: "helo"))
persion.name = "jackson"
print(persion.name)
Copy the code

Where we have syntax prompts when we type persion.

This is because the compiler can query all the targets and their types from the Key path. Because of this, it is very suitable for packaging types:

@dynamicMemberLookup
struct Wrapper<Content> {
    var content: Content
    subscript<Value> (dynamicMember keyPath: WritableKeyPath<Content.Value>) -> Value {
        get {
            return content[keyPath: keyPath]
        }
        set {
            content[keyPath: keyPath] = newValue
        }
    }
}
// We can access properties directly from Wrapper
      
        as Scene
      
var scene2 = Wrapper(content: Scene())
scene2.title = "Scene"
Copy the code

Chain transformation

From the implementation of the Scene chain call above, we can easily see that all we need to do to implement the chain is return self when we’re done.


@dynamicMemberLookup
struct Setter<Subject> {
    let subject: Subject
    
    subscript<Value> (dynamicMember keyPath: WritableKeyPath<Subject.Value>)- > ((Value) - >Setter<Subject>) {
        
        // Get the real object
        var subject = self.subject
        
        return { value in
            // Assign value to subject
            subject[keyPath: keyPath] = value
            // The type of return is Setter instead of Subject
            // Because setters are used to chain, not Subject itself
            return Setter(subject: subject)
        }
    }
}
Copy the code

Then, any object instance can be set chaining by wrapping it in a Setter:

Setter(subject: Scene()) / / packaging Scene ()
    .title("Scene3")  // Set the title
    .backgroudColor(.red) // Set the background
    .subject  // Read the last changed object
Copy the code

Quickly rewrite UIView for UIKit:

Setter(subject: UIView())
    .frame(CGRect(x: 0, y: 0, width: 100, height: 100))
    .backgroundColor(.white)
    .alpha(0.5)
    .subject
Copy the code

Nice extension of UIView call method, 🐱 wow