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

栏目: Swift · 发布时间: 6年前

内容简介:SwiftUI 应用了许多 Swift 5.1 的新特性。在上一篇中,我们聊了Swift UI 中修饰 View 状态的属性的Swift 中两个叫做 "Key Path" 的特性,一个是我们今天着重聊的是另一个 Swift Smart Key Path

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 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 实例中了。

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

两个 KeyPath 可以拼接成一个新的 KeyPath:

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 的类型变成了 ReferenceWritableKeyPath

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

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

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

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Agile Web Development with Rails 4

Agile Web Development with Rails 4

Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95

Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码