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

掃描下方二維碼,關注「面試官小健」

相關文章
相關標籤/搜索