SwiftUI 和 Swift 5.1 新特性(3) Key Path Member Lookup

SwiftUI 应用了许多 Swift 5.1 的新特性。在上一篇中,咱们聊了Swift UI 中修饰 View 状态的属性的 @Binding@State 的本质是属性代理。在本文中,咱们将了解Swift @Binding@State 类型背后包含的另外一个特性:Key Path Member Lookup。咱们首先要复习一下 Swift 中两个平常开发中不太经常使用的特性:KeyPath 和 Dynamic Member Lookup。面试

1.KeyPath 而不是 #keyPath

Swift 中两个叫作 "Key Path" 的特性,一个是 #keyPath(Person.name),它返回的是String类型,一般用在传统 KVO 的调用中,addObserver:forKeyPath: 中,若是该类型中不存在这个属性,则会在编译的时候提示你。swift

咱们今天着重聊的是另外一个 Swift Smart Key Path KeyPath<Root,Value>,这是个泛型类型,用来表示从 Root 类型到 某个 Value 属性的访问路径,咱们来看一个例子安全

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

let keyPath = \Person.name
let p = Person(name: "Leon", age: 32)
print(p[keyPath: keyPath]) // 打印出 Leon
复制代码

咱们看到获取 KeyPath 的实例须要用到一个特殊的语法 \Person.name,它的类型是 KeyPath<Person, String>,在使用 KeyPath 的地方,可使用下标操做符来使用这个 keyPath。上面是个简单的例子,而它的实际用途须要跟泛型结合起来:闭包

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

// 给 Sequence 添加 KeyPath 版本的方法
extension Sequence {
  func map<Value>(keyPath: KeyPath<Element,Value>) -> [Value] {
    return self.map{ $0[keyPath: keyPath]}
  }
}

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

复制代码

在没有添加新的map方法的时候,获取[Person]中全部 name 的方式是提供一个闭包,在这个闭包中咱们须要详细写出如何获取的访问方式。而在提供了 KeyPath 版本的实现后,只须要提供个强类型的 KeyPath 实例便可,如何获取这件事情已经包含在了这个 KeyPath 实例中了。app

同一类型的 KeyPath<Root,Value> 能够表明多种获取路径。例如:KeyPath<Person, Int> 既能够表示 \Person.age 也能够表示 \Person.name.count框架

两个 KeyPath 能够拼接成一个新的 KeyPath:ide

let keyPath = \Person.name
let keyPath2 = keyPath.appending(path: \String.count)
复制代码

keyPath 的类型是 KeyPath<Person,String>\String.count 的类型是 KeyPath<String,Int>,调用 appending 函数,变成一个 KeyPath<Person, Int> 的类型。函数

咱们再来看一下继承关系:KeyPathWriteableKeyPath 的父类,WriteableKeyPathReferenceWritableKeyPath 的父类。从继承关系咱们能够推断出,要知足 is a 的原则,KeyPath 的能力是最弱的:只能以只读的方式访问属性; WriteableKeyPath 能够对可变的值类型的可变属性进行写入:将第一个代码示例中的 let 都改为 var,那么 Person.name 的类型就变成了 WriteableKeyPath,那么能够写 p[keyPath: keyPath] = "Bill" 了;ReferenceWritableKeyPath 能够对引用类型的可变属性进行写入:将第一个代码示例中的 struct 改为 classPerson.name 的类型变成了 ReferenceWritableKeyPathpost

此外,还有两种不经常使用的 KeyPath:PartialKeyPath<Root>KeyPath 的父类,它擦除了 Value 的类型,以及PartialKeyPath<Root>的父类 AnyKeyPath 则将全部的类型都擦除了。这五种 KeyPath 类型使用了 OOP 保持了继承的关系,所以可使用 as? 进行父类到子类的动态转换。ui

KeyPath 机制常被用来对属性作类型安全访问的地方:在增删改查的 ORM 框架里很常见,另外一个例子就是SwiftUI了。

2. 动态成员查找 Dynamic Member Lookup

Dynamic Member Lookup 是 Swift 4.2 中引入的特性,目的是使用静态的语法作动态的查找,示例以下:

@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()
let age: Int = p.hello // 32
let name: String = p.name // Leon
复制代码

支持 Dynamic Member Lookup 的类型首先须要用 @dynamicMemberLookup 来修饰,动态查找方法须要命名为 subscript(dynamicMember member: String),能够根据不一样的返回类型重载。

使用方面,能够直接使用.propertyname的语法来貌似静态实则动态地访问属性,实际上调用的是上面的方法。若是如上例有重载,为了消除二义性,得经过返回值类型,明确调用方法。

3. Key Path Member Lookup 成员查找

复习完了 KeyPath 和 Dynamic Member Lookup,咱们来到了 Swift 5.1 中引入的 Key Path Member Lookup。这里咱们先引入一个类型 Lens,它封装了对值存取的功能:

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

复制代码

这时候,咱们但愿结合以前复习的 KeyPath 提供一个方法:将对这个值的存取,结合入参 KeyPath,转换成对于 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 })
  }
}

// 使用 project 方法
func projections(lens: Lens<Person>) {
  let lens = lens.project(\.name)   // Lens<String>
}

复制代码

为了让一个 Lens 更美观地转换成另外一种 Lens,须要语法上的突破,这时候框架和语言做者又想到了 Dynamic Member Lookup 了,因为 4.2 中只支持 String 做为参数的调用,调用.property 的语法。若是把它扩展到 支持 KeyPath 为入参,调用的时候再施加编译器的魔法,变成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 })
  }
}

复制代码

上面是应用 Swift 5.1 Key Path Member Lookup 后的最终版本。咱们能够用 .property 的语法获得了一个新的能够对值存取的实例。lens.name 这时候等价于 lens[dynamicMember:\.name],咱们能够用更精简的语法完成转换。

4. @State 和 @Binding 都如 Lens

上一篇文章咱们提到:StateBinding 类型定义处有 @propertyDelegate 的修饰,这篇咱们看到的是:它们都还有 @dynamicMemberLookup的修饰,证据是它们都有这个方法:subscript<Subject>(dynamicMember: WritableKeyPath<Value, Subject>) -> Binding<Subject>,为的是对于值的存取能够优雅地结合 KeyPath 进行转换。 咱们来看如下代码:

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)
      }
    }
  }
}
复制代码

上面是一段 SwiftUI 的代码,在 SwiftUI 中 View 的具体类型是值类型,它表明了对 View 的描述。当此处的 isEditing 或者 Slide 发生修改的时候,SwiftUI 会从新生成这个 Viewbody

有关 slide 属性的绑定咱们看到了两个用法:在读取的时候直接用 propertyWrapper 给到你的 slide.number 就能够了,而当这个绑定须要向下传给另外一个子控件的时候,则使用上次和今天介绍的两个特性:先使用 $slide 获取到 slide 被代理到的 Binding 实例,而后使用 .title的语法获取到新的 Binding<String>TextFiled 的初始化函数的第一个参数,正是Binding<String>

Binding<T> 设计很是重要,它贯穿了 SwiftUI 的控件设计,咱们能够想像若是设计一个 ToggleButton,则初始化函数必定有一个 Binding<Bool>。从抽象层面上来讲,Binding 它抽象了对某个值的存取,但并不须要知道这个值到底是如何存取的,十分巧妙。

StateBinding稍有不一样,它首先表明了一个实实在在的 View 的内部状态(由SwiftUI 管理)苹果称之为 Source of Truth 的一种,而 Binding 明显表达了一种引用的关系。固然 State 也能使用 Key Path Member Lookup 的语法变成一个 Binding

结语

在本文中,咱们结合上篇内容,详细讲述了 StateBinding 背后的另外一个新语言特性 Key Path Member Lookup,而且了解了这样设计的精妙之处。

咱们已经花了 3 篇文章来聊 SwiftUI 和 Swift 5.1 的新特性,可是这尚未结束,上面代码示例中的 VStack 的内部是合法的 Swift 代码吗?咱们下次再聊。

相关文章:

SwiftUI 和 Swift 5.1 新特性(1) 不透明返回类型 Opaque Result Type

SwiftUI 和 Swift 5.1 新特性(2) 属性代理Property Delegates

扫描下方二维码,关注“面试官小健”

相关文章
相关标签/搜索