Write better swift code: chained calls and @ dynamicmemberlookup

OldBirds 2021-09-15 08:26:30

In use UIKit When , We will write a lot of similar code :

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

But in SwiftUI in :

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

I wonder if your taste is the same as mine , I think the latter is more aesthetic than the former , More concise . The former needs to pass imageView Object to set , It's old-fashioned . The chain style of the latter , Smooth and fast , No temporary variables are needed to manipulate this object , With one .

Ordinary thinking

If you want to achieve a similar chain style , We need to manually modify the corresponding properties .

for instance , If there is such a class :

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

Scene To achieve Chain , You need to add corresponding methods for these properties :

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

By sending back self, Implement chain call :

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

This method , Quite passive , If Scene Property changes , Then the corresponding setting method has to be changed . Proper labor costs .

But if Swift5.1 Above version , There is another option .

Dynamic Member Lookup

stay Swift4.2 At version time ,Swift Added Dynamic Member Lookup, Dynamic member query . In the use of @dynamicMemberLookup After marking the object ( object 、 Structure 、 enumeration 、protocol), Realized subscript(dynamicMember member: String) Method, we can access properties that do not exist in the object . If the accessed property does not exist , Will be called to the implementation subscript(dynamicMember member: String) Method ,key As member Pass in this method .

for instance , If the structure originally defined :

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

add to @dynamicMemberLookup after :

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

next , We can be like direct access Persion The properties of the object are the same , access info The content of :

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

from Introduce User-defined "Dynamic Member Lookup" Types As can be seen from the proposal , This function is designed to work with Python And other dynamic languages .

But why with call chaining associated , Because in Swift5.1, This feature has been upgraded .

Key Path Member Lookup

stay Swift5.1 in , Besides strings , It can also be used. key path As a medium for dynamic member query .

Suppose we put Persion The definition is as follows :

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

Then add Key path member lookup after :

@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 code 

Now except through persion.info.name To set the external , Sure :

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

One of us when we knock persion. When , There are grammatical hints .

Screen Shot 2021-09-08 at 8.17.57 P

This is because the compiler can start from Key path To query all the targets , And their types . Officially 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
}
}
}
// You can directly Wrapper<Scene> As a Scene To access properties 
var scene2 = Wrapper(content: Scene())
scene2.title = "Scene"
 Copy code 

Chain conversion

From above Scene Implementation of chained call , We can easily know , To achieve chaining is nothing more than after setting , return self.


@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
// hold value Assign to subject
subject[keyPath: keyPath] = value
// The type of return is Setter instead of Subject
// Because use Setter Come chain , instead of Subject In itself 
return Setter(subject: subject)
}
}
}
 Copy code 

next , Just use any object instance with Setter wrap up , You can set it by chain :

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

Quickly rewrite UIKit Of UIView:

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

Happily expanded UIView How to invoke , wow

Please bring the original link to reprint ,thank
Similar articles

2021-09-15