SwiftUI 應用了許多 Swift 5.1 的新特性。在上一篇中,咱們聊了Swift UI 中修飾 View 狀態的屬性的 @Binding
和 @State
的本質是屬性代理。在本文中,咱們將瞭解Swift @Binding
和 @State
類型背後包含的另外一個特性:Key Path Member Lookup。咱們首先要複習一下 Swift 中兩個平常開發中不太經常使用的特性:KeyPath 和 Dynamic Member Lookup。面試
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>
的類型。函數
咱們再來看一下繼承關係:KeyPath
是 WriteableKeyPath
的父類,WriteableKeyPath
是 ReferenceWritableKeyPath
的父類。從繼承關係咱們能夠推斷出,要知足 is a
的原則,KeyPath
的能力是最弱的:只能以只讀的方式訪問屬性; WriteableKeyPath
能夠對可變的值類型的可變屬性進行寫入:將第一個代碼示例中的 let
都改爲 var
,那麼 Person.name
的類型就變成了 WriteableKeyPath
,那麼能夠寫 p[keyPath: keyPath] = "Bill"
了;ReferenceWritableKeyPath
能夠對引用類型的可變屬性進行寫入:將第一個代碼示例中的 struct
改爲 class
則 Person.name
的類型變成了 ReferenceWritableKeyPath
post
此外,還有兩種不經常使用的 KeyPath:PartialKeyPath<Root>
是 KeyPath
的父類,它擦除了 Value
的類型,以及PartialKeyPath<Root>
的父類 AnyKeyPath
則將全部的類型都擦除了。這五種 KeyPath 類型使用了 OOP 保持了繼承的關係,所以可使用 as?
進行父類到子類的動態轉換。ui
KeyPath 機制常被用來對屬性作類型安全訪問的地方:在增刪改查的 ORM 框架裏很常見,另外一個例子就是SwiftUI了。
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
的語法來貌似靜態實則動態地訪問屬性,實際上調用的是上面的方法。若是如上例有重載,爲了消除二義性,得經過返回值類型,明確調用方法。
複習完了 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]
,咱們能夠用更精簡的語法完成轉換。
上一篇文章咱們提到: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
掃描下方二維碼,關注「面試官小健」