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 的組織方式。

public init(content: () -> Content)

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

image

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

不過在使用 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 當然也可以接受動態方式的輸入,這時使用的初始化方法和上面靜態的情況不一樣:

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

    //...
}

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

  • 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 的類型信息,可以看到下面的下面的信息:

(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 時會十分有用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM