SwiftUI:The Shortest Path to a Great App

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 在哪裏?數據庫

First Glance

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 寫一個列表頁進行對比:編程

  • 不須要實現 UITableViewDelegate 和 UITableViewDataSource,寫一堆冗長的代碼,在 List(items) 中描述列表的數據,在 List 的 Closure 中描述每一個 Cell
  • 不須要使用 AutoLayout 或 Frame 對元素進行排版,HStack(View Container)將元素包起來,簡單清晰
  • 當數據 landmarks 有變化時,不須要再調用 reloadData,包括 landmarks 個數有變化或 landmark.isFavorite 值變化,SwiftUI 都會自動更新界面

能夠看到,SwiftUI 極大地簡化了構建 UI 的過程(Faster),這種耳目一新的構建方式是 Declarative 聲明式編程(Modern),而以前 UIKit 的方式是 Imperative 命令式編程,二者有什麼區別呢?swift

Imperative vs Declarative

  • Imperative:命令式,明確而詳細的告訴機器作一些事情,從而達到你想要的結果,專一於 How。這種方式更貼近機器思惟,CPU 就是一條條執行 PC 指向的機器碼。
  • Declarative:聲明式,描述你想要什麼,交由機器來來完成你想要的,專一於 What。這種方式更貼近人類思惟,最開始都是先肯定本身想要什麼,纔會一步步實現。

舉個例子,若是咱們要去旅遊:app

  • 對於 Imperative,就是自由行,本身要安排詳細的行程,包括購買機票,查詢各類交通,預約酒店,預約遊玩場所的門票,肯定吃飯的餐廳等等。
  • 對於 Declarative,就是跟團遊,本身只須要表達想去哪裏玩,旅行社或者代理商會幫你安排整個行程。

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。

View

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。

Swift 5.1 Magic for SwiftUI DSL

爲了實現 SwiftUI 的聲明式編程,提供 DSL,Swift 語言在 5.1 中引入了一些新特性:(注:這一節的內容參考 SwiftUI 的一些初步探索 (一) - 小專欄SwiftUI 的 DSL 語法分析 - 知乎 較多)

Opaque Return Types

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")
        }
    }
}
複製代碼

Property Delegates

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 自動更新的效果。

Trailing Closure & Function Builder

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

相關文章
相關標籤/搜索