SwiftUI學習(二)

教程 2 - Building Lists and Navigation

Section 4 - Step 2: 靜態 List

var body: some View {
    List {
        LandmarkRow(landmark: landmarkData[0])
        LandmarkRow(landmark: landmarkData[1])
    }
}

這裏的 List 和 HStack 或者 VStack 之類的容器很類似,接受一個 view builder 並採用 View DSL 的方式列舉了兩個 LandmarkRow。這種方式構建了對應着 UITableView 的靜態 cell 的組織方式。git

public init(content: () -> Content)

咱們能夠運行 app,並使用 Xcode 的 View Hierarchy 工具來觀察 UI,結果可能會讓你以爲很眼熟:github

image

實際上在屏幕上繪製的 UpdateCoalesingTableView 是一個 UITableView 的子類,而兩個 cell ListCoreCellHost 也是 UITableViewCell 的子類。對於 List 來講,SwiftUI 底層直接使用了成熟的 UITableView 的一套實現邏輯,而並不是從新進行繪製。相比起來,像是 Text 或者 Image 這樣的單一 View 在 UIKit 層則所有統一由 DisplayList.ViewUpdater.Platform.CGDrawingView 這個 UIView 的子類進行繪製。spring

不過在使用 SwiftUI 時,咱們首先須要作的就是跳出 UIKit 的思惟方式,不該該去關心背後的繪製和實現。使用 UITableView 來表達 List 也許只是權宜之計,也許在將來也會被另外更高效的繪製方式取代。因爲 SwiftUI 層只是 View 描述的數據抽象,所以和 React 的 Virtual DOM 以及 Flutter 的 Widget 同樣,背後的具體繪製方式是徹底解耦合,而且能夠進行替換的。這爲從此 SwiftUI 更進一步留出了足夠的可能性。編程

Section 5 - Step 2: 動態 List 和 Identifiable

List(landmarkData.identified(by: \.id)) { landmark in
    LandmarkRow(landmark: landmark)
}

除了靜態方式之外,List 固然也能夠接受動態方式的輸入,這時使用的初始化方法和上面靜態的狀況不同:swift

public struct List<Selection, Content> where Selection : SelectionManager, Content : View {
    public init<Data, RowContent>(
        _ data: Data, action: @escaping (Data.Element.IdentifiedValue) -> Void,
        rowContent: @escaping (Data.Element.IdentifiedValue) -> RowContent) 
    where 
        Content == ForEach<Data, Button<HStack<RowContent>>>, 
        Data : RandomAccessCollection, 
        RowContent : View, 
        Data.Element : Identifiable

    //...
}

這個初始化方法的約束比較多,咱們一行行來看:api

  • Content == ForEach<Data, Button<HStack<RowContent>>> 由於這個函數簽名中並無出現 ContentContent僅只 List<Selection, Content> 的類型聲明中有定義,因此在這與其說是一個約束,不如說是一個用來反向肯定 List 實際類型的描述。如今讓咱們先將注意力放在更重要的地方,稍後會再多講一些這個。
  • Data : RandomAccessCollection 這基本上等同於要求第一個輸入參數是 Array
  • RowContent : View 對於構建每一行的 rowContent 來講,須要返回是 View 是很正常的事情。注意 rowContent 其實也是被 @ViewBuilder 標記的,所以你也能夠把 LandmarkRow 的內容展開寫進去。不過通常咱們會更但願儘量拆小 UI 部件,而不是把東西堆在一塊兒。
  • Data.Element : Identifiable 要求 Data.Element (也就是數組元素的類型) 上存在一個能夠辨別出某個實例的知足 Hashable 的 id。這個要求將在數據變動時快速定位到變化的數據所對應的 cell,並進行 UI 刷新。

關於 List 以及其餘一些常見的基礎 View,有一個比較有趣的事實。在下面的代碼中,咱們指望 List 的初始化方法生成的是某個類型的 View數組

var body: some View {
    List {
        //...
    }
}

可是你看遍 List 的文檔,甚至是 Cmd + Click 到 SwiftUI 的 interface 中查找 View 相關的內容,都找不到 List : View 之類的聲明。閉包

難道是由於 SwiftUI 作了什麼手腳,讓原本沒有知足 View 的類型均可以「充當」一個 View 嗎?固然不是這樣…若是你在運行時暫定 app 並用 lldb 打印一下 List 的類型信息,能夠看到下面的下面的信息:app

(lldb) type lookup List
...
struct List<Selection, Content> : SwiftUI._UnaryView where ...

進一步,_UnaryView 的聲明是:框架

protocol _UnaryView : View where Self.Body : _UnaryView {
}

SwiftUI 內部的一元視圖 _UnaryView 協議雖然是知足 View 的,但它被隱藏起來了,而知足它的 List 雖然是 public 的,可是卻能夠把這個協議鏈的信息也做爲內部信息隱藏起來。這是 Swift 內部框架的特權,第三方的開發者沒法這樣在在兩個 public 的聲明之間插入一個私有聲明。

最後,SwiftUI 中當前 (Xcode 11 beta 1) 只有對應 UITableView 的 List,而沒有 UICollectionView 對應的像是 Grid 這樣的類型。如今想要實現相似效果的話,只能嵌套使用 VStack 和 HStack。這是比較奇怪的,由於技術層面上應該和 table view 沒有太多區別,大概是由於工期不太夠?相信從此應該會補充上 Grid

教程 3 - Handling User Input

Section 3 - Step 2: @State 和 Binding

@State var showFavoritesOnly = true

var body: some View {
    NavigationView {
        List {
            Toggle(isOn: $showFavoritesOnly) {
                Text("Favorites only")
            }
    //...
            if !self.showFavoritesOnly || landmark.isFavorite {

這裏出現了兩個之前在 Swift 裏沒有的特性:@State 和 $showFavoritesOnly

若是你 Cmd + Click 點到 State 的定義裏面,能夠看到它實際上是一個特殊的 struct

@propertyWrapper public struct State<Value> : DynamicViewProperty, BindingConvertible {

    /// Initialize with the provided initial value.
    public init(initialValue value: Value)

    /// The current state value.
    public var value: Value { get nonmutating set }

    /// Returns a binding referencing the state value.
    public var binding: Binding<Value> { get }

    /// Produces the binding referencing this state value
    public var delegateValue: Binding<Value> { get }
}

@propertyWrapper 標註和上一篇中提到的 @_functionBuilder 相似,它修飾的 struct 能夠變成一個新的修飾符並做用在其餘代碼上,來改變這些代碼默認的行爲。這裏 @propertyWrapper 修飾的 State 被用作了 @State 修飾符,並用來修飾 View 中的 showFavoritesOnly 變量。

和 @_functionBuilder 負責按照規矩「從新構造」函數的做用不一樣,@propertyWrapper 的修飾符最終會做用在屬性上,將屬性「包裹」起來,以達到控制某個屬性的讀寫行爲的目的。若是將這部分代碼「展開」,它其實是這個樣子的:

// @State var showFavoritesOnly = true
   var showFavoritesOnly = State(initialValue: true)

var body: some View {
    NavigationView {
        List {
//          Toggle(isOn: $showFavoritesOnly) {
            Toggle(isOn: showFavoritesOnly.binding) {
                Text("Favorites only")
            }
    //...
//          if !self.showFavoritesOnly || landmark.isFavorite {
            if !self.showFavoritesOnly.value || landmark.isFavorite {

我把變化以前的部分註釋了一下,而且在後面一行寫上了展開後的結果。能夠看到 @State 只是聲明 State struct 的一種簡寫方式而已。State 裏對具體要如何讀寫屬性的規則進行了定義。對於讀取,很是簡單,使用 showFavoritesOnly.value 就能拿到 State 中存儲的實際值。而原代碼中 $showFavoritesOnly 的寫法也只不過是 showFavoritesOnly.binding 的簡化。binding 將建立一個 showFavoritesOnly 的引用,並將它傳遞給 Toggle。再次強調,這個 binding 是一個引用類型,因此 Toggle 中對它的修改,會直接反應到當前 View 的 showFavoritesOnly 去設置它的 value。而 State 的 value didSet 將觸發 body 的刷新,從而完成 State -> View 的綁定。

在 Xcode 11 beta 1 中,Swift 中使用的修飾符名字是 @propertyDelegate,不過在 WWDC 上 Apple 提到這個特性時把它叫作了 @propertyWrapper。根據可靠消息,在將來正式版中應該也會叫作 @propertyWrapper,因此你們在看各類資料的時候最好也建議一個簡單的映射關係。

若是你想要了解更多關於 @propertyWrapper 的細節,能夠看看相關的提案論壇討論。比較有意思的細節是 Apple 在將相應的 PR merge 進了 master 之後又把這個提案的打回了「修改」的狀態,而非直接接受。除了 @propertyWrapper 的名稱修正之外,應該還會有一些其餘的細節修改,可是已經公開的行爲模式上應該不會太大變化了。

SwiftUI 中還有幾個常見的 @ 開頭的修飾,好比 @Binding@Environment@EnvironmentObject 等,原理上和 @State 都同樣,只不過它們所對應的 struct 中定義讀寫方式有區別。它們共同構成了 SwiftUI 數據流的最基本的單元。對於 SwiftUI 的數據流,若是展開的話足夠一整篇文章了。在這裏仍是十分建議看一看 Session 226 - Data Flow Through SwiftUI 的相關內容。

教程 5 - Animating Views and Transitions

Section 2 - Step 4: 兩種動畫的方式

在 SwiftUI 中,作動畫變的十分簡單。Apple 的教程裏提供了兩種動畫的方式:

  1. 直接在 View 上使用 .animation modifier
  2. 使用 withAnimation { } 來控制某個 State,進而觸發動畫。

對於只須要對單個 View 作動畫的時候,animation(_:) 要更方便一些,它和其餘各種 modifier 並無太大不一樣,返回的是一個包裝了對象 View 和對應的動畫類型的新的 Viewanimation(_:) 接受的參數 Animation 並非直接定義 View 上的動畫的數值內容的,它是描述的是動畫所使用的時間曲線,動畫的延遲等這些和 View 無關的東西。具體和 View 有關的,想要進行動畫的數值方面的變動,由其餘的諸如 rotationEffect 和 scaleEffect 這樣的 modifier 來描述。

在上面的 教程 5 - Section 1 - Step 5 裏有這樣一段代碼:

Button(action: {
    self.showDetail.toggle()
}) {
    Image(systemName: "chevron.right.circle")
        .imageScale(.large)
        .rotationEffect(.degrees(showDetail ? 90 : 0))
        .animation(nil)
        .scaleEffect(showDetail ? 1.5 : 1)
        .padding()
        .animation(.spring())
}

要注意,SwiftUI 的 modifier 是有順序的。在咱們調用 animation(_:) 時,SwiftUI 作的事情等效因而把以前的全部 modifier 檢查一遍,而後找出全部知足 Animatable 協議的 view 上的數值變化,好比角度、位置、尺寸等,而後將這些變化打個包,建立一個事物 (Transaction) 並提交給底層渲染去作動畫。在上面的代碼中,.rotationEffect 後的 .animation(nil) 將 rotation 的動畫提交,由於指定了 nil 因此這裏沒有實際的動畫。在最後,.rotationEffect 已經被處理了,因此末行的 .animation(.spring()) 提交的只有 .scaleEffect

withAnimation { } 是一個頂層函數,在閉包內部,咱們通常會觸發某個 State 的變化,並讓 View.body 進行從新計算:

Button(action: {
    withAnimation {
        self.showDetail.toggle()
    }
}) { 
  //...
}

若是須要,你也能夠爲它指定一個具體的 Animation

withAnimation(.basic()) {
    self.showDetail.toggle()
}

這個方法至關於把一個 animation 設置到 View 數值變化的 Transaction 上,並提交給底層渲染去作動畫。從原理上來講,withAnimation 是統一控制單個的 Transaction,而針對不一樣 View 的 animation(_:) 調用則可能對應多個不一樣的 Transaction

教程 7 - Working with UI Controls

Section 4 - Step 2: 關於 View 的生命週期

ProfileEditor(profile: $draftProfile)
    .onDisappear {
        self.draftProfile = self.profile
    }

在 UIKit 開發時,咱們常常會接觸一些像是 viewDidLoadviewWillAppear 這樣的生命週期的方法,並在裏面進行一些配置。SwiftUI 裏也有一部分這類生命週期的方法,好比 .onAppear 和 .onDisappear,它們也被「統一」在了 modifier 這面大旗下。

可是相對於 UIKit 來講,SwiftUI 中能 hook 的生命週期方法比較少,並且相對要通用一些。自己在生命週期中作操做這種方式就和聲明式的編程理念有些相悖,看上去就像是加上了一些命令式的 hack。我我的比較期待 View 和 Combine能再深度結合一些,把像是 self.draftProfile = self.profile 這類依賴生命週期的操做也用綁定的方式搞定。

相比於 .onAppear 和 .onDisappear,更通用的事件響應 hook 是 .onReceive(_:perform:),它定義了一個能夠響應目標 Publisher 的任意的 View,一旦訂閱的 Publisher 發出新的事件時,onReceive 就將被調用。由於咱們能夠自行定義這些 publisher,因此它是完備的,這在把現有的 UIKit View 轉換到 SwiftUI View 時會十分有用。

相關文章
相關標籤/搜索