SwiftUI 能夠說是 WWDC 2019 中最讓人激動的技術了,什麼是 SwiftUI 呢?官方說法爲:SwiftUI is a modern way to declare user interfaces for any Apple platform. Create beautiful, dynamic apps faster than ever before。html
總之,這套新的 UI 框架用 WWDC Session 中的話描述就是:git
The Shortest Path to a Great Appgithub
那下面咱們就用 SwiftUI 實現一個 iOS 中最多見的列表頁,看看到底 Modern、Faster 在哪裏?數據庫
struct LandMarkView : View {
let landmarks: [LandMark]
var body: some View {
List(landmarks) { landmark in
HStack {
Image(landmark.thumbnail)
Text(landmark.name)
Spacer()
if landmark.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
}
}
}
複製代碼
同使用 UIKit 寫一個列表頁進行對比:編程
List(items)
中描述列表的數據,在 List 的 Closure 中描述每一個 Cell能夠看到,SwiftUI 極大地簡化了構建 UI 的過程(Faster),這種耳目一新的構建方式是 Declarative 聲明式編程(Modern),而以前 UIKit 的方式是 Imperative 命令式編程,二者有什麼區別呢?swift
舉個例子,若是咱們要去旅遊:app
Declarative 的核心在於描述 What,將 How 委託給一個 Expert 來完成。如何描述 What,這裏就涉及到了 DSL 領域描述語言。框架
在 SwiftUI 以前,咱們其實或多或少接觸過 Declarative,最典型的就是 SQL,SQL 語句就是一種 DSL,例如對於 SELECT * from product WHERE id = 996
這條語句,只是描述了咱們想從 product 表中找到 id 爲 996 的商品(What),至於怎麼找(How),交給數據庫來處理,數據庫會高效、健壯的取到數據並返回給咱們。另外,AutoLayout 也能夠當作一種簡單的 Declarative,咱們描述約束,Layout Engine 計算最終的 Frame。ide
Imperative 和 Declarative 二者各有優缺點,從目前的趨勢來看,React/Flutter/SwiftUI 經過 Declarative 來構建 UI,看起來 Declarative 是將來 UI 編程的趨勢。爲何你們都不約而同的選擇 Declarative 呢?今年 WWDC 中 Apple 工程師給出了答案:函數
對於一個 App 而言,其代碼分爲兩部分 Basic Features 和 Exciting/Custom Features,讓 App 出彩、給用戶帶來很棒體驗的是 Exciting/Custom Features,SwiftUI 的目的就是爲了減小開發者在 Basic Features 部分的負擔,讓開發者更專一於 Exciting/Custom Features。
A view defines a piece of UI
上面也提到了,聲明式至關於將具體的操做委託給一個 Engine,由 Engine 來作具體的髒活累活,向上提供一個抽象層。在 SwiftUI 中這個抽象層就是 View,SwiftUI 中的 View 再也不是 UIKit 中的 UIView,沒有 Backing Store,不涉及到真正的渲染,View 只是一個抽象概念,描述 UI 應該如何展現。咱們看下 View 的定義:
public protocol View : _View {
associatedtype Body : View
var body: Self.Body { get }
}
複製代碼
能夠看出,在 SwiftUI 中 View 只是一個 protocol,裏面有一個 body 的屬性,body 又是 View。這樣就能夠經過 body 將 View 串起來,造成 View Hierarchy。
爲了實現 SwiftUI 的聲明式編程,提供 DSL,Swift 語言在 5.1 中引入了一些新特性:(注:這一節的內容參考 SwiftUI 的一些初步探索 (一) - 小專欄 和 SwiftUI 的 DSL 語法分析 - 知乎 較多)
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
複製代碼
上面一段自定義 View 的代碼中 var body: some View
這行中多了一個 some
,這個 some 是幹嘛用的?因爲 View 只是 protocol,在 Swift 5.1 以前,帶有 associatedtype 的協議是不能作爲類型來用,只能做爲類型約束:
// Error
// Protocol 'View' can only be used as a generic constraint
// because it has Self or associated type requirements
func createView() -> View {
}
// OK
func createView<T: View>() -> T {
}
複製代碼
至關於在聲明 body 時,不能用 View,須要指定具體的類型,例如 VStack、Text 等,但若是 body 的類型變化,每次都須要修改,比較麻煩。所以 Swift 5.1 引入了 Opaque Return Types,使用方式是 some protocol
,當 body 的類型變成 some View
後,至關於它向編譯器做出保證,每次 body 獲得的必定是某一個肯定的、遵照View協議的類型,可是請編譯器「網開一面」,不要再細究具體的類型。返回類型肯定單一這個條件十分重要,寫成下面的樣子編譯器會報錯:
// Error
// Function declares an opaque return type, but the return
// statements in its body do not have matching underlying types
let someCondition: Bool = false
var body: some View {
if someCondition {
return Text("Hello World")
} else {
return Button(action: {}) {
Text("Tap me")
}
}
}
複製代碼
struct RoomDetail : View {
let room: Room
@State private var zoomed = false
var body: some View {
Image(room.imageName)
.resizable()
.aspectRatio(contentMode: zoomed ? .fill : .fit)
.tapAction { self.zoomed.toggle() }
}
}
複製代碼
在上面的代碼中,一旦 zoomed 的值發生變化,SwiftUI 會自動更新 UI,這一切都源於 @State
。State 本質上只是一個自定義類,用 @propertyDelegate 修飾,@State var zoomed
會將 zoomed 的讀寫轉到 State 類中實現了。
@propertyDelegate public struct State<Value> @propertyDelegate public struct Binding<Value> @propertyDelegate public struct Environment<Value> 複製代碼
裏面 @propertyDelegate
是 Swift 5.1 引入的新特性 Property Delegate,這個特性有什麼用呢?假設咱們有一個設置頁面,須要在 UserDefault 中存儲一些屬性,
struct Preferences {
static var shouldAlert: Bool {
get {
return UserDefaults.standard.object(forKey: "shouldAlert") as? Bool ?? false
} set {
UserDefaults.standard.set(newValue, forKey: "shouldAlert")
}
}
static var refreshRequency: Bool {
get {
return UserDefaults.standard.object(forKey: "refreshRequency") as? TimeInterval ?? 6000
} set {
UserDefaults.standard.set(newValue, forKey: "refreshRequency")
}
}
複製代碼
能夠發現 shouldAlert 和 refreshRequency 代碼重複較多,若是再多一些設置值,Preferences 這個類會寫的煩死。針對這種狀況,Swift 5.1 引入 Property Delegate,能夠將 Property 的相同行爲 Delegate 給一個代理對象去作:
@propertyDelegate
struct UserDefault<T> {
let key: String
let defaultValue: T
var value: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
struct Preferences {
@UserDefault(key: "shouldAlert", defaultValue: false)
static var shouldAlert: Bool
@UserDefault(key: "refreshRequency", defaultValue: 6000)
static var refreshRequency: TimeInterval
}
複製代碼
當使用 @UserDefault(key: "shouldAlert", defaultValue: false)
修飾過 shouldAlert 以後,shouldAlert 會被編譯器處理成下面的樣子:
struct Preferences {
static var $shouldAlert = UserDefault<Bool>(key: "shouldAlert", defaultValue: false)
static var shouldAlert: Bool {
get {
return $shouldAlert.value
}
set {
$shouldAlert.value = newValue
}
}
}
複製代碼
回到 @State
,當 zoomed 被 @State
修飾後,zoomed 的讀寫被 Delegate 到 State 類中,SwiftUI 框架在 State 類中根據 zoomed 值的變化去觸發界面的更新,達到 Value 變化 UI 自動更新的效果。
HStack(alignment: .center) {
Image(landmark.thumbnail)
Text(landmark.name)
Spacer()
if landmark.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
複製代碼
HStack 中 View 與 View 之間沒有 , 區分,也沒有 return,這種 DSL 的寫法主要基於 Swift 的 Trailing Closure 和 Function Builder。下面是 HStack 的定義:
public struct HStack<Content> where Content : View {
@inlinable public init(alignment: VerticalAlignment = .center,
spacing: Length? = nil,
@ViewBuilder content: () -> Content)
複製代碼
首先對於 Trailing Closure,若是一個 Swift 方法中最後一個參數是 Closure,則能夠將 Closure 提到括號外面。
@_functionBuilder public struct ViewBuilder {
public static func buildBlock() -> EmptyView
public static func buildBlock(_ content: Content) -> Content where Content : View
}
複製代碼
其次對於 Function Builder,能夠看到 content 前面有一個 @ViewBuilder
,而 ViewBuilder 使用了 @_functionBuilder
修飾,被 @ViewBuilder
修飾過的 Closure 就會被修改語法樹,轉調 ViewBuilder 的 buildBlock 函數。最終
HStack(alignment: .center) {
Image(landmark.thumbnail)
Text(landmark.name)
Spacer()
if landmark.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
複製代碼
被轉換成了
HStack(alignment: .center) {
return ViewBuilder.buildBlock(
Image(landmark.thumbnail),
Text(landmark.name),
Spacer()
)
}
複製代碼
最後,Apple 爲 SwiftUI 提供了一個不管是內容仍是交互都很是棒的官方教程,值得學習 SwiftUI 時跟着教程動手練習,正如同今年 WWDC 的主題同樣,一塊兒 Write Code,Blow Minds 吧。
Article by JoeShang