SwiftUI applies many of the new features of Swift 5.1. In the last article, we talked about the @binding and @state properties that modify View State in the Swift UI that are essentially property proxies. In this article, we’ll look at another feature contained behind the Swift @Binding and @state types: the Key Path Member Lookup. We’ll start with a review of two features in Swift that are less commonly used in daily development: KeyPath and Dynamic Member Lookup.

1.KeyPath instead of # KeyPath

The two “Key Path” features in Swift are #keyPath(person.name), which returns a String and is usually used in traditional KVO calls, addObserver:forKeyPath: If this attribute is not present in the type, it will prompt you at compile time.

KeyPath KeyPath

. This is a generic type that represents the access Path from the Root type to a Value property. Let’s look at an example
,value>

struct Person {
  let name: String
  let age: Int
}

let keyPath = \Person.name
let p = Person(name: "Leon", age: 32)
print(p[keyPath: keyPath]) // Print out Leon
Copy the code

We see that the KeyPath instance requires a special syntax, \ person. name, which is of type KeyPath . In place of KeyPath, you can use the subscript operator to use the KeyPath. The above is a simple example, but its practical use needs to be combined with generics: ,>

// Before
print(people.map{$0.name })

// Add the KeyPath version to the Sequence
extension Sequence {
  func map<Value>(keyPath: KeyPath<Element,Value>)- > [Value] {
    return self.map{$0[keyPath: keyPath]}
  }
}

// After
print(people.map(keyPath:\.name))

Copy the code

Without adding new map methods, the way to get all the names in [Person] is to provide a closure where we need to detail how to get access. After providing the KeyPath version implementation, all you need to do is provide a strongly typed KeyPath instance, which is already included in the KeyPath instance.

The same type of KeyPath

can represent multiple access paths. For example, KeyPath can represent either \ person. age or \ person.name.count. ,>
,value>

Two KeyPath can be spliced together into a new KeyPath:

let keyPath = \Person.name
let keyPath2 = keyPath.appending(path: \String.count)
Copy the code

KeyPath is of type keyPath , and \ string. count is of type keyPath

. Become a KeyPath type. ,>
,int>
,string>

Let’s look at the inheritance relationship again: KeyPath is the parent of WriteableKeyPath, and WriteableKeyPath is the parent of ReferenceWritableKeyPath. From inheritance we can infer that KeyPath has the weakest ability to satisfy the principle of IS A: properties can be accessed only in read-only mode; WriteableKeyPath can be used to write variable properties of variable value types: change the let in the first code example to var, and the person. name type becomes WriteableKeyPath, so you can write p[keyPath: KeyPath] = “Bill”; ReferenceWritableKeyPath can write mutable properties of reference types: changing the struct in the first code example to class changes the person. name type to ReferenceWritableKeyPath

In addition, there are two less common types of KeyPath: PartialKeyPath

, which is the parent of KeyPath, which erases the Value type, and PartialKeyPath

, which erases all types. The five KeyPath types use OOP to maintain an inherited relationship, so you can use as? Dynamic conversion from parent class to child class.

The KeyPath mechanism is often used to do type-safe access to properties: it is common in add-delete ORM frameworks, and SwiftUI is another example.

2. Dynamic Member Lookup

Dynamic Member Lookup Dynamic Member Lookup Dynamic Member Lookup is a feature introduced in Swift 4.2 to perform Dynamic Lookup using static syntax, as shown in the following example:

@dynamicMemberLookup
struct Person {
  subscript(dynamicMember member: String) - >String {
    let properties = ["name": "Leon"."city": "Shanghai"]
    return properties[member, default: "null"]}subscript(dynamicMember member: String) - >Int {
    return 32}}let p = Person(a)let age: Int = p.hello / / 32
let name: String = p.name // Leon
Copy the code

DynamicMemberLookup @dynamicMemberLookup Dynamic MemberLookup subscript(dynamicMember Member Member: String), which can be overloaded depending on the return type.

With aspects, you can use the.propertyName syntax directly to access properties statically but dynamically, instead of calling the above methods. If the above example is overloaded, to eliminate ambiguity, call the method explicitly by returning the value type.

3. Key Path Member Lookup Member Lookup

Having reviewed KeyPath and Dynamic Member Lookup, we moved on to the KeyPath Member Lookup introduced in Swift 5.1. Here we introduce a type Lens that encapsulates access to values:

struct Lens<T> {
  let getter: () -> T
  let setter: (T) - >Void
  
  var value: T {
    get {
      return getter()
    }
    nonmutating set {
      setter(newValue)
    }
  }
}

Copy the code

At this point, we want to combine KeyPath with the previous review to provide a method that converts the access to this value, combined with the KeyPath parameter, to the access to the property type specified by KeyPath.

extension Lens {
  func project<U>(_ keyPath: WritableKeyPath<T, U>) -> Lens<U> {
    return Lens<U>(
      getter: { self.value[keyPath: keyPath] },
      setter: { self.value[keyPath: keyPath] = $0}}})// Use the project method
func projections(lens: Lens<Person>) {
  let lens = lens.project(\.name)   // Lens<String>
}

Copy the code

In order to convert one Lens to another Lens in a more elegant way, the authors of the framework and language came up with the idea of Dynamic Member Lookup, because 4.2 only supports the invocation of String as a parameter, and the syntax of calling.property. Wouldn’t it be nice to extend it to KeyPath and apply compiler magic to lens.name?

@dynamicMemberLookup
struct Lens<T> {
  let getter: () -> T
  let setter: (T) - >Void

  var value: T {
    get {
      return getter()
    }
    nonmutating set {
      setter(newValue)
    }
  }

  subscript<U>(dynamicMember keyPath: WritableKeyPath<T.U- > >)Lens<U> {
    return Lens<U>(
        getter: { self.value[keyPath: keyPath] },
        setter: { self.value[keyPath: keyPath] = $0}}})Copy the code

Swift 5.1 Key Path Member Lookup Swift 5.1 Key Path Member Lookup We can use the.property syntax to get a new instance of value access. Lens. name is now equivalent to lens[dynamicMember:\.name], which can be converted using a more concise syntax.

4. @binding is like Lens

As we mentioned in the last article, the @propertyDelegate modifier is in the State and Binding type definitions. DynamicMemberLookup subscript

(dynamicMember: WritableKeyPath

) -> Binding

so that access to values can be elegantly converted with KeyPath. Let’s look at the following code:

,>

struct SlideViewer: View {@State private var isEditing = false
  @Binding var slide: Slide

  var body: some View {
    VStack {
      Text("Slide #\(slide.number)")
      if isEditing {
        TextFiled($slide.title)
      }
    }
  }
}
Copy the code

In SwiftUI, the specific type of View is the value type, which represents the description of the View. When there is an isEditing or Slide change, SwiftUI will regenerate the body of the View.

We have seen two uses for binding the slide property: just pass the propertyWrapper directly to your slide.number on reading, and when the binding needs to be passed down to another child control, use the two features described last time and today: TextFiled’s initialization function takes the first argument, which is Binding

. TextFiled’s initialization function takes the first argument, which is Binding

.

Binding

design is very important, it is used throughout the SwiftUI control design, we can imagine that if we design a ToggleButton, the initialization function must have a Binding

. At an abstract level, Binding abstracts access to a value without knowing exactly how the value is accessed.

State is slightly different from Binding. It first represents the internal State of an actual View (managed by SwiftUI). Apple calls it Source of Truth, while Binding clearly expresses a reference relationship. Mostly used as initialization parameters for control apis. Of course, a State can also become a Binding using a projection attribute (for example, $isEditing).

conclusion

In this article, we take a closer look at Key Path Member Lookup, another new language feature behind State and Binding, and see the subtleties of this design.

We’ve spent three articles talking about the new SwiftUI and Swift 5.1 features, but that’s not the end of the story. Is the inside of the VStack in the above code example legitimate Swift code? We’ll talk to you next time.

Related articles:

SwiftUI and Swift 5.1 New Features (1) Opaque Return Type Opaque Result Type

Property Delegates – New features in Swift 5.1 (2) Property Delegates

Scan the QR code below to follow “Interviewer Xiaojian”