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
實際上在屏幕上繪製的 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 更進一步留出了足夠的可能性。編程
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>>>
由於這個函數簽名中並無出現 Content
,Content
僅只 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
。
@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 的相關內容。
在 SwiftUI 中,作動畫變的十分簡單。Apple 的教程裏提供了兩種動畫的方式:
View
上使用 .animation
modifierwithAnimation { }
來控制某個 State
,進而觸發動畫。對於只須要對單個 View
作動畫的時候,animation(_:)
要更方便一些,它和其餘各種 modifier 並無太大不一樣,返回的是一個包裝了對象 View
和對應的動畫類型的新的 View
。animation(_:)
接受的參數 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
。
View
的生命週期ProfileEditor(profile: $draftProfile) .onDisappear { self.draftProfile = self.profile }
在 UIKit 開發時,咱們常常會接觸一些像是 viewDidLoad
,viewWillAppear
這樣的生命週期的方法,並在裏面進行一些配置。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 時會十分有用。