内容简介: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>
的类型。
我们再来看一下继承关系: KeyPath
是 WriteableKeyPath
的父类, WriteableKeyPath
是 ReferenceWritableKeyPath
的父类。从继承关系我们可以推断出,要满足 is a
的原则, KeyPath
的能力是最弱的:只能以只读的方式访问属性; WriteableKeyPath
可以对可变的值类型的可变属性进行写入:将第一个代码示例中的 let
都改成 var
,那么 Person.name
的类型就变成了 WriteableKeyPath
,那么可以写 p[keyPath: keyPath] = "Bill"
了; ReferenceWritableKeyPath
可以对引用类型的可变属性进行写入:将第一个代码示例中的 struct
改成 class
则 Person.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
上一篇文章我们提到: State
和 Binding
类型定义处有 @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 会重新生成这个 View
的 body
。
有关 slide
属性的绑定我们看到了两个用法:在读取的时候直接用 propertyWrapper 给到你的 slide.number
就可以了,而当这个绑定需要向下传给另一个子控件的时候,则使用上次和今天介绍的两个特性:先使用 $slide
获取到 slide 被代理到的 Binding
实例,然后使用 .title
的语法获取到新的 Binding<String>
, TextFiled
的初始化函数的第一个参数,正是 Binding<String>
。
Binding<T>
设计非常重要,它贯穿了 SwiftUI 的控件设计,我们可以想像如果设计一个 ToggleButton
,则初始化函数一定有一个 Binding<Bool>
。从抽象层面上来说, Binding
它抽象了对某个值的存取,但并不需要知道这个值究竟是如何存取的,十分巧妙。
State
与 Binding
稍有不同,它首先代表了一个实实在在的 View 的内部状态(由SwiftUI 管理)苹果称之为 Source of Truth 的一种,而 Binding
明显表达了一种引用的关系。当然 State
也能使用 Key Path Member Lookup 的语法变成一个 Binding
。
结语
在本文中,我们结合上篇内容,详细讲述了 State
和 Binding
背后的另一个新语言特性 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
扫描下方二维码,关注“面试官小健”
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 『互联网架构』软件架构-redis特性和集群特性(中)(49)
- 『互联网架构』软件架构-redis特性和集群特性(上)(48)
- 『互联网架构』软件架构-redis特性和集群特性(下)(50)
- JDK 14 功能特性
- C# 特性(Attribute)
- python—高级特性
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Functional Programming in Scala
Paul Chiusano、Rúnar Bjarnason / Softbound print / 2014-9-14 / USD 44.99
Functional programming (FP) is a programming style emphasizing functions that return consistent and predictable results regardless of a program's state. As a result, functional code is easier to test ......一起来看看 《Functional Programming in Scala》 这本书的介绍吧!