今年蘋果的WWDC你看了嗎?蘋果在2019年的WWDC的重頭戲當然非SwiftUI莫屬:全新的聲明式語法、綁定式API、和響應式變成框架Combine。這一切的一切都預示著即將在Apple Native布局系統掀起一場革命。為此,蘋果在很多方面都做了努力,這才促成了SwiftUI現在的樣子。想要了解Swift的新特性、SwiftUI數據流和SwiftUI布局系統等新知識嗎?一起來看吧。

Swift 5.1 新語法

單表達式隱式返回值

在 Swift 5.0 之前的語法中,如果一個閉包表達式只有一個表達式,那麼可以省略 return 關鍵字。 現在 Swift 5.1 以後的版本中計算屬性和函數語句同樣適用。 如:

// before swift 5.0

struct Rectangle {
var width = 0.0, height = 0.0
var area1: Double {
return width * height
}

func area2() -> Double {
return width * height
}
}

// after switft 5.1
struct Rectangle {
var width = 0.0, height = 0.0
var area1: Double { width * height }

func area2() -> Double { width * height }
}

關於這個新特性的完整提案可以參考(github.com/apple/Swift-

根據結構體默認成員合成默認初始化器

在 Swift 5.0 之前結構體聲明,編譯器會默認生成一個逐一成員初始化器,同時如果都有默認值,還會生成一個無參的初始化器。 但如果此時結構體成員屬性過多,且較多都有默認值,則只能使用逐一成員初始化器,會使每處調用的地方寫法過於冗餘,在傳統 OOP 語言中可以使用 Builder 模式解決, 但在 Swift 5.1 之後編譯器會按需合成初始化器,避免初始化寫法的冗餘。 如:

struct Dog {
var name = "Generic dog name"
var age = 0
}
let boltNewborn = Dog()
let daisyNewborn = Dog(name: "Daisy", age: 0)
// before swift 5.0 ?
let benjiNewborn = Dog(name: "Benji")
// after switft 5.1 ?
let benjiNewborn = Dog(name: "Benji")

關於這個新特性的完整提案可以參考(github.com/apple/Swift-

字元串插入運算符新設計

這個特性主要擴大了字元串插入運算符的使用範圍,以前我們只能用在 String 的初始化中,但是不能在參數處理中使用字元串插入運算符。 在以前的語法中只能分開書寫,雖然沒什麼大問題,但總歸要多一行代碼,現在可以直接使用了, 尤其是對於 SwiftUI,Text 控制項就使用到了這種新語法,可以使我們在單行表達式中即可初始化 Tetx。

// before swift 5.0 ?
let quantity = 10
label.text = NSLocalizedString(
"You have (quantity) apples,
comment: "Number of apples"
)

label.text = String(format: formatString, quantity)

// after switft 5.1 ?
let quantity = 10
return Text(.
"You have (quantity) apples"
).

// 實際上編譯器會翻譯為如下幾句
var builder = LocalizedStringKey.StringInterpolation(
literalCapacity: 16, interpolationCount: 1
)
builder.appendLiteral("You have ")
builder.appendInterpolation(quantity)
builder.appendLiteral(" apples")
LocalizedStringKey(stringInterpolation: builder)

關於這個新特性的完整提案可以參考(github.com/apple/Swift-

屬性包裝器

當我們在一個類型中聲明計算屬性時,大部分屬性的訪問和獲取都是有相同的用處,這些代碼是可抽取的,如我們標記一些用戶偏好設置,在計算屬性的設置和獲取中直接代理到 UserDefault的實現中,我們可以通過聲明 @propertyWarpper 來修飾,可以減少大量重複代碼。 在 SwiftUI中, @State @EnviromemntObject @bindingObject @Binding 都是通過屬性包裝器代理到 SwiftUI 框架中使其自動響應業務狀態的變化。

// before swift 5.0 ?
struct User {
static var usesTouchID: Bool {
get {
return UserDefaults.standard.bool(forKey: "USES_TOUCH_ID")
}
set {
UserDefaults.standard.set(newValue, forKey: "USES_TOUCH_ID")
}
}
static var isLoggedIn: Bool {
get {
return UserDefaults.standard.bool(forKey: "LOGGED_IN")
}
set {
UserDefaults.standard.set(newValue, forKey: "LOGGED_IN")
}
}
}

// after switft 5.1 ?
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
print("UserDefault init")
self.key = key
self.defaultValue = defaultValue
UserDefaults.standard.register(defaults: [key: defaultValue])
}
var value: T {
get {
print("getter")
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
print("setter")
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
struct User2 {
@UserDefault("USES_TOUCH_ID", defaultValue: false)
static var usesTouchID: Bool
@UserDefault("LOGGED_IN", defaultValue: false)
var isLoggedIn: Bool
}

print("hello world")
let user = User2()
User2.usesTouchID = true
let delegate = User2.$usesTouchID
print("(delegate)")
let detelate2 = user.$isLoggedIn

實際上屬性包裝器是在編譯時期翻譯為以下的代碼, 並且編譯器禁止使用 $ 開頭的標識符。

struct User2 {
static var $usesTouchID = UserDefault<Bool>("USES_TOUCH_ID", defaultValue: false)
static var usesTouchID: Bool {
set {
$usesTouchID.value = newValue
}
get {
$usesTouchID.value
}
}
@UserDefault("LOGGED_IN", defaultValue: false)
var isLoggedIn: Bool
}

使用屬性包裝器的好處除了可以減少重複代碼,Swift Runtime 還保證了以下幾點:

  • 對於實例的屬性包裝器是即時載入的
  • 對於類屬性的屬性保證器是懶載入的
  • 屬性包裝器是線程安全的
  • 通過 $ 運算符可以獲取到原始的屬性包裝器實例,這大量使用在 SwiftUI 的數據依賴中

關於這個新特性的完整提案可以參考(github.com/apple/swift-

註:在目前的 Beta 版本中 @PropertyDelegate@PropertyWrapper 是一致的,正式版本發布會應該只會保留一種語法。

不透明返回類型

在 Swift 5.0 之前我們如果想返回抽象類型一般使用 Generic Type 或者 Protocol, 使用泛型會顯示的暴露一些信息給 API 使用者,不是完整的類型抽象。 但是使用 Protocol 也有幾個限制: 泛型返回值在運行時都是一個容器,效率較差,返回值不能調用自身類型的方法,協議不允許擁有關聯類型,由於編譯時丟失了類型信息,編譯器無法推斷類型,導致無法使用 == 運算符。

在 Swift 5.1 中新增了 opaque result type,這個特性使用 some 修飾協議返回值,具有以下特性:

  • 所有的條件分支只能返回一個特定類型,不同則會編譯報錯
  • 方法使用者依舊無法知道類型,(使用方不透明)
  • 編譯器知情具體類型,因此可以使用類型推斷。

// before swift 5.0 ?
public protocol View : _View {
associatedtype Body : View

var body: Self.Body { get }
}
// compile error
func getText() -> View {
return Text("")
}

func getText<T: Text>() -> T {
return Text("")
}
// after switft 5.1 ?
struct ContentView: View {
var body: some View {
Text("")
}
}

關於這個新特性的完整提案可以參考(github.com/apple/Swift-

Swift Style DSL / Function Builder

此項提案是一個比較特殊的提案,原因在於目前還在審核中,並沒有正式加入 Swift 語言中,但是 Xcode11 自帶的 Swift 5.1 已經集成了這項語法. 使用 Function Builder 可以使表達式語句隱含的返回在函數返回值中, 這樣可以在嵌套邏輯和返回值表達式的 DSL 中十分具有表現力。如下面的語句是等同的:

// Original source code:
@TupleBuilder
func build() -> (Int, Int, Int) {
1
2
3
}

// This code is interpreted exactly as if it were this code:
func build() -> (Int, Int, Int) {
let _a = 1
let _b = 2
let _c = 3
return TupleBuilder.buildBlock(_a, _b, _c)
}

在 SwiftUI 中的所有布局類控制項,幾乎全部使用了 Function Builder 特性,可讀性要遠遠大於 Flutter 的 DSL語法,以下是一個蘋果 WWDC Session 的例子。

head {
meta().charset("UTF-8")
if cond {
title("Title 1")
} else {
title("Title 2")
}
}

實際上會被翻譯為
head {
let a: HTML = meta().charset("UTF-8")
let d: HTML
if cond {
let b: HTML = title("Title 1")
d = HTMLBuilder.buildEither(first: b)
} else {
let c: HTML = title("Title 2")
d = HTMLBuilder.buildEither(second: c)
}
return HTMLBuilder.buildBlock(a, d)
}

可讀性大大加強,但值得注意的是目前在 SwiftUI 中使用 Function Builder 實現的 ViewBuilder 功能僅僅支持 10 個泛型參數,在 SwiftUI 中,如果同級別元素中,超過 10 個則會有奇怪的編譯錯誤。

這點官方推薦使用組合降低 View 結構,這點和 Flutter 的推薦寫法不同,超過 10 個 則會編譯錯誤 -_-。

目前這份提議[function-builders.md](github.com/apple/swift-),還在草案階段,所以實現是一份下劃線關鍵字。

不排除蘋果會在 9月份正式版本來臨之前稍加修改語法,但相信這份功能的放開也會在正式版本發布之期修改完畢。

其他新特性

本文主要介紹了一些新的語法特性,且大部分都和 SwiftUI 相關,從 WWDC 的Session 演講者都透露出 Swift 語言組的核心目標,Make Your Swift API Better

主要從以下幾個體現。

  • Expressive 有表現力
  • Clear 清晰沒二義性
  • Easy to use 簡單易用

本文還有大量的 Swift 5.1 新特性沒有提到,如 @dynamicCallable @dynamicMemberLookup WritableKeyPath 有興趣的讀者可以參考 Swift的完整演變提案 swift-evolution。(github.com/apple/swift-

Swift 從 3.x Attribute

Swift 自 3.0 版本至今添加了很多 屬性(Attribut) 標記,這些 Attribute 通過給編譯器提供信息,增強了 Swift 的元編程能力,如果讀者有興趣可以參考這份完整的正式版本 Attribute大全。(docs.swift.org/swift-bo

Swift/SwiftUI API Design Guide

Swift 已經經過 9(4年內部孵化) 個年頭的發展,語言的設計指南已經區域問題,其中涉及到了很多不同於其他語言的觀點,今年的 Session 著重的梳理了以下幾點。

值類型和引用類型

當你設計一個數據結構時,優先選擇結構體或者枚舉,它們都是值類型,值類型具有以下幾個有點。

1.值類型在棧上分配,性能要遠遠大於引用類型,且 Swift Runtime 有 COW 優化。

2.值類型沒有引用計數,不會引起奇怪的多線程安全問題。

3.值類型的存儲屬性是扁平化的,避免在類繼承情況下一個子類繼承過多的存儲屬性導致實例在內存中過大,如 SwiftUI 使用 Modifier的結構體優化設計。

那什麼時候我們才需要使用引用類型呢? 只有當以下幾個場景存在時才有必要使用。

  • 你需要引用計數和構造和析構的時機。
  • 數據需要集中管理或共享,如單例或者數據緩存實例等。
  • ID 語義和 Equal語義衝突的時候。

協議還是泛型

隨著 Swift 中 POP 的流行,很多人涉及 API 越來越習慣使用 協議,但是官方指出設計的要點, 設計是演進的而不是一步到位的,應該遵守以下步驟。

1、不要直接聲明協議

2、從實際的業務場景出發

3、從中抽象出公用代碼

4、從已有的協議中組合協議,而不是重新構建一套新的協議結構

DynamicMemberLookup & dynamicCallable

這個語法其實是 Swift 4.2 的語法了,但是在 Xcode11 中, IDE進一步獲得了增強,可以輕鬆獲得代碼提示,如果使用過 ruby 和 python的開發者,可以輕易地理解,把一個實例當方法調用, 靜態語言在保證安全的前提下可以通過編譯,並可以動態調用,相信未來在和其他語言互通時可以大方光彩。

抽象數據訪問

前面已經介紹過來 Swift 5.1 新增語法 PropertyWarpper, 在 Swift 中很多計算屬性都圍繞著數據訪問如 懶載入,防禦性複製,TLS數據區,而它們的模式都是一致的, 多使用 PropertyWarpper 可以抽象公用代碼,加強 API 語義。 在 SwiftUI 中核心的數據流,均使用 PropertyWarpper 如 @Binding @State @EnviromentObject @Enviroment。

SwiftUI 360° 分析

在 SwiftUI 中我們不再使用傳統的 Cocoa 命令式布局系統,該用聲明式布局,那麼到底什麼事聲明式布局?什麼是命令式布局?。 舉個例子,假設你要設計如下布局。

在命令式布局系統中,你要做以下事情,

  • 初始化一個 ImageView 實例
  • 設置它的位置信息
  • 設置它的縮放級別
  • 把它添加到當前的視圖樹中
  • 如果有事件,設置下事件的代理或者回掉

開發者要做的事情繁多且易錯,開發效率極為原始低下,但在聲明式時代你只需要描述一下信息。

屏幕上有個圖片,在什麼位置,是什麼縮放比例即可, 至於怎麼布局,一切交給框架,Coder do less, Framework do more。

至於聲明式編程和命令式編程,可以參考維基百科 聲明式編程 命令式編程。 實際上聲明式編程早在上個世紀就已經提出並且演化很久,和命令式不同的是,聲明式編程一般要求框架做的事情非常多,在早期計算機性能一般的年代,聲明式編程並未大火, 一般多用於解析 DSL (Domain Specific Language) 中,比如早期的 HTML 布局,SQL 語言等。

順便提下早在 2006 年微軟便提出 WPF 框架,其中使用 XAML 語言編寫聲明式 UI 代碼,同時支持事件/數據綁定機制,配合宇宙第一 IDE Visual Studio 強大的拖拉拽功能,開發者體驗爽到機制。 但是遺憾的是 Window Phone 並未在移動操作系統平台爭得一番天地,以至於很多人對聲明式 UI 不甚了解。

聲明式 UI 會是未來嗎?

結合最近多年大火的前端大火的框架 React 移動平台的 Reactive Native WeexFlutter 包括暫時只能編寫Native平台的 SwiftUIJetpack Compose. 多方的數據論證和流行度可以說明,聲明式編程在 UI 布局方面有得天獨厚的優勢, 相信未來 UI 布局也會是聲明式編程的天下。

SwiftUI 中的 View

對於傳統的的 Cocoa 編程中,一個 View 可能是一個 UIView 也可能是一個 NSView 代表了屏幕上可視的元素,且依賴於對應的平台。 而在 SwiftUI 的實現中 A View Defines a Piece of UI 一個 View 是一個真實屏幕上可見元素的描述,在不同的平台可以是 UIView 也可以說 NSView 或 其他實現, View 是跨平台的描述信息,底層的實現被封裝在框架內部實現。

SwiftUI View & Modifier

在傳統的命令式編程布局系統中,我們對一些 UI 系統結構是通常是通過繼承實現的,再編寫代碼時通過對屬性的調用來修改視圖的外觀,如顏色透明度等。 但這會帶來導致類繼承結構比較複雜,如果設計不夠好會造成 OOP 的通病類爆炸,並且通過繼承來的數據結構,子類會集成父類的存儲屬性,會導致子類實例在內存佔據比較龐大,即便很多屬性都是默認值並不使用。 如圖:

在 SwiftUI 中獎視圖的修飾期抽象為 Modifier, Modifier通常只含有 1-2 個存儲屬性,通過原始控制項的 Extension 可以在視圖定義中添加各種 Modifier,它們在運行時的實現通常是一個閉包,在運行時 SwiftUI 構建出真實的視圖。

SwiftUI 元控制項

在 SwiftUI 系統中我們使用結構體遵守 View 協議,通過組合現有的控制項描述,實現 Body 方法,但 Body 的方法會不會無限遞歸下去?

在 SwiftUI 系統中定義了 6 個元/主 View Text Color Spacer Image Shape Divider, 它們都不遵守 View 協議,只是基本的視圖數據結構。

其他常見的視圖組件都是通過組合元控制項和修飾器來組合視圖結構的。如 Button Toggle 等。

關於 SwiftUI 的視圖和修飾器可以參考 Github 整理的速查表Jinxiansen/SwiftUI。(github.com/Jinxiansen/S

DataFlow in SwiftUI

任何程序都不可能是靜態的,充滿著數據狀態,函數充滿著副作用,傳統的命令式編程通過成員變數來管理狀態,這使得狀態的複雜度成指數級增長。舉一個最簡單的例子。假設一個視圖只有 4 種狀態,組合起來就有16種, 但是人腦處理狀態的複雜度是有限的,狀態的複雜度一旦超過人腦的複雜度,就會產生大量的 Bug,並且修掉了這個產生了那個,試問哪位同學看見一個類充斥著幾十上百個成員屬性和全局變數不想拍桌子的。

在 Flutter 中控制項分為StateLessStateFull 控制項,數據流是單向的,隨之誕生了 Bloc RxDart Redux 許多框架,雖然作者本人是 SwiftUI 的粉絲,但不得不說 Flutter 的很多思想非常先進。 這裡簡單分析下 SwiftUI 中如何處理完們的數據流。 這裡先放出數據流的原則,我們關注兩個點 Source of Truth 是指我們真是的業務邏輯數據,Dervied Value 是指 SwiftUI 框架中使用的數據。

Constant

通常部分 UI 數據是不可改變的,比如一個 Text 的 Color, 這部分我們可以直接使用 Modifier 的構造器講屬性傳遞進去即可,這裡就不做多解釋了。

Text("Hello world")
.color(.red)

@State

那在某些情況下我們某些 UI 元素會有一系列的響應事件,會導致視圖發現變化,那麼 SwiftUI 是怎麼做到的? 就是通過前面的新語法 Property warpper, 對所有使用 State 標記的屬性 都會代理到 SwiftUI 中 State 的方法實現,同時框架,計算Diff,刷新界面。 順便這裡強調下在 SwiftUI 中 Views area a function of State not of a sequence of Event, View 是真實視圖的狀態,並不是一系列變化的事件。

struct PlayerView: View {
let episode: Episode
@State private var isPlaying = false
var body: some View {
VStack {
Text(episode.title)
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.cicle" : "play.circle")
}
}
}
}

@Binding

再很多時候我們會總歸抽象減少我們的代碼長度,比如將上文中的 Button 抽象為一個 PlayerButton , 這時候存在一個問題,State 屬性我們是再重新聲明一份嗎?

struct PlayButton: View {
@State private var isPlaying = false
var body: some View {
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.cicle" : "play.circle")
}
}
}

這樣寫會有一個問題,我們會產生兩個 Derived Value,雖然兩者都可通知 SwiftUI 做界面刷新,但是 PlayerViewPlayerButton 的數據同步又成了問題。

SwiftUI 推薦使用 @Binding 解決,我們來看下 Binding 的實現。

@propertyDelegate public struct Binding<Value> {
public var value: Value { get nonmutating set }

/// Initializes from functions to read and write the value.
public init(getValue: @escaping () -> Value, setValue: @escaping (Value) -> Void)

}

Binding 結構體使用閉包捕獲了原本的屬性值,使得屬性可以用引用的方式保留。

struct PlayerView: View {
let episode: Episode
@State private var isPlaying = false
var body: some View {
VStack {
Text(episode.title)
PlayButton($isPlaying)
}
}
}

這裡 State 實現了 BindingConvertible 協議,使得 State 可以直接轉換為 Binding。

@BingableObject & Combine

UI 除了受用戶點擊影響 有時候還來自於外部的通知,如一個IM類消息,收到遠程的消息,或者一個定時器被觸發。

在 SwiftUI 中通過最新的 Combine 框架可以很方便的響應外部變化,開發者只需實現 BindableObject 協議即可。

class PodcastPlayerStore: BindableObject {
var didChange = PassthoughSubject<Void, Never>()
func advance() {
// ..
didChange.send()
}
}

struct PlayerView: View {
let episode: Episode
@State private var isPlaying = false
@State private var currentTime: TimeInterval = 0.0
var body: some View {
VStack {
Text(episode.title)
PlayButton($isPlaying)
}
.onReceive(PodcastPlayerStore.currentTimePublisher) { newTime in
self.currentTime = newTime
}
}
}

@ObjectBinding

使用 State 的方式可以通知單個視圖的變化,但是有時候我們需要多個視圖共享一個元素信息,並且在數據信息發送變化時通知 SwiftUI 刷新所有布局,這時候可以使用

@ObjectBinding。

final class PodcastPlayer: BindableObject {
var isPlaying: Bool = false {
didSet {
didChange.send(self)
}
}

func play() {
isPlaying = true
}

func pause() {
isPlaying = false
}

var didChange = PassthroughSubject<PodcastPlayer, Never>()
}

struct EpisodesView: View {
@ObjectBinding var player: PodcastPlayer
let episodes: [Episode]

var body: some View {
List {
Button(
action: {
if self.player.isPlaying {
self.player.pause()
} else {
self.player.play()
}
}, label: {
Text(player.isPlaying ? "Pause": "Play")
}
)
ForEach(episodes) { episode in
Text(episode.title)
}
}
}
}

@propertyDelegate public struct ObjectBinding<BindableObjectType> : DynamicViewProperty where BindableObjectType : BindableObject {
@dynamicMemberLookup public struct Wrapper {
public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<BindableObjectType, Subject>) -> Binding<Subject> { get }
}

public var value: BindableObjectType

public init(initialValue: BindableObjectType)

public var delegateValue: ObjectBinding<BindableObjectType>.Wrapper { get }

public var storageValue: ObjectBinding<BindableObjectType>.Wrapper { get }
}

系統通過 ObjectBinding<BindableObjectType>.Wrapper 感知外部數據的變化。

@EnviromemntObject

有時候一些環境變數是共享的,我們可以通過 EnviromentObject 獲取共享的信息,這些共享的數據信息回沿著 View 樹的的結構向下傳遞。 類似於 Flutter 的 Theme 和 ScopeModel ,比較簡單這裡就不多做解釋了。

let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: LandmarkList().environmentObject(UserData()))

struct LandmarkList: View {
@EnvironmentObject private var userData: UserData

var body: some View {
Text("")
}

@Enviroment

前面提到的環境信息一般是指用戶自定義共享信息,但是同時系統存在大量的內置環境信息,如時區,顏色模式等,可以直接訂閱系統的環境信息,使得 SwiftUI 自動獲取到環境信息的變化,自動刷新布局。

struct CalendarView: View {
@Environment(.calendar) var calendar: Calendar
@Environment(.locale) var locale: Locale
@Environment(.colorScheme) var colorScheme: ColorScheme

var body: some View {
return Text(locale.identifier)
}
}

總結

以上是 SwiftUI 官方在各種教程和 WWDC session 提到的數據流管理方案,總結下來是以下幾點:

  • 對於不變的常量直接傳遞給 SwiftUI 即可。
  • 對於控制項上需要管理的狀態使用 @State 管理。
  • 對於外部的事件變化使用 BindableObject 發送通知。
  • 對於需要共享的視圖可變數據使用 @ObjectBinding 管理。
  • 不要出現多個狀態同步管理,使用 @Binding 共享一個 Source of truth
  • 對於系統環境使用 @Enviroment 管理。
  • 對於需要共享的不可變數據使用 @EnviromemntObject 管理。
  • @Binding 具有引用語義,可以很好的和 @Binding @objectBinding @State 協作,避免出現多個數據不同步。

SwiftUI 的布局演算法

相信很多人看過 SwiftUI 後對 SwiftUI 內部的布局演算法是比較好奇的,在 Session 237 Building Custom Views with SwiftUI 摘要介紹了一下。 SwiftUI 會通過 body 的返回值獲取描述視圖的控制項信息,轉換為對應的內部視圖信息,交給 2D 繪圖引擎 Metal 或者 Open GL 繪製,其中比較複雜的 Toggle 可能引用自原本的UIKit實現。

Content View

對於以下的代碼:

import SwiftUI

struct ContentView : View {
var body: some View {
Text("Hello World!")
}
}

它們的結構是如下的 RootView -> ContentView -> Text, 那麼 Text 是如何出現在屏幕上的?官方的介紹是如下三個步驟

1、父視圖為子視圖提供預估尺寸

2、子視圖計算自己的實際尺寸

3、父視圖根據子視圖的尺寸將子視圖放在自身的坐標系中

比較重要的是第二步,對於一個視圖描述,通常有三種設置尺寸的方式。

  • 無需計算,根據內容推斷,如 Image 是和圖片等大,Text 是計算出來的可視範圍,類似 NSString 根據字體計算寬高。
  • Frame 強制指定寬高
  • 設置縮放比例 如 Image 設置 aspectRatio。

順便蘋果提了一點

SwiftUI 中將計算出的模糊坐標點會對齊到清晰的像素點,避免出現鋸齒感。

那麼對於使用 Modifier 的布局結構如下:

import SwiftUI

struct ContentView : View {
var body: some View {
Text("Hello World!")
.padding(10)
.background(Color.Green)
}
}
struct Avocado : View {
var body: some View {
Image("20x20_avocado")
.frame(width: 30, height: 30)
}
}

看上去它們 Frame 和 Background Padding 等元素都出現在了視圖結構中,但它們其實都是 View 的約束信息,並不是真正的 View。

HStack/VStack

HStack 和 ZStack 的非常類似安卓的 LinerLayout,演算法也同 Flex 布局比較相似。 對於如下的布局, 蘋果都會在控制項之間添加上符合蘋果人機交互指南的間距,保證 UI 的優雅和一致性。

HStack {
VStack {
Text("")
Text("5 stars")
}
.font(.caption)
VStack {
HStack {
Text("Avocado Toast").font(.title)
Spacer()
Image("20x20_avocado")
}
Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
.font(.caption)
.lineLimit(1)
}
}

對於如上的 Stack 是怎麼計算的?設 Stack 主軸方向長度為 W1。

1、根據人機交互指南的預留出邊距 S, 邊距根據元素的排列可能有多個

2、得到剩餘的主軸寬度 W2= W1 - N * S

3、平均分配一個預估寬度

4、計算一些具備明確寬高的元素 如 Image 設置了 Frame的元素的等。

5、沿主軸方向從前到後計算,,如果計算出來的寬度小於預估寬度則正常顯示,不夠則截斷。

6、最後的元素為剩餘寬度,如果不夠顯示則階段

7、默認的交叉軸對齊方式為 Center,Stack 佔據包括最大元素的邊界。

默認的計算是順序計算布局,如果某些元素比較重要,可以使用 LayoutPriority Modifier 提高布局優先順序避免出現視圖截斷。

交叉軸對齊方式

很多時候我們開發布局系統會嵌套很多 HStack 和 ZStack 有時候我們除了內部對齊,還有按照自定義的對齊比例的對齊。

extension VerticalAlignment {
public static let top: VerticalAlignment
public static let center: VerticalAlignment
public static let bottom: VerticalAlignment
public static let firstTextBaseline: VerticalAlignment
public static let lastTextBaseline: VerticalAlignment
}

extension HorizontalAlignment {

public static let leading: HorizontalAlignment
public static let center: HorizontalAlignment
public static let trailing: HorizontalAlignment
}

extension VerticalAlignment {
private enum MidStarAndTitle: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> Length {
return d[.bottom]
}
}
static let midStarAndTitle = VerticalAlignment(MidStarAndTitle.self)
}

// 自定義對齊方式
HStack(alignment: .midStarAndTitle) {
VStack {
Text("")
Text("5 stars")
}
.font(.caption)
VStack {
HStack {
Text("Avocado Toast").font(.title)
Spacer()
Image("20x20_avocado")
}
Text("Ingredients: Avocado, Almond Butter, Bread, Red Pepper Flakes")
.font(.caption)
.lineLimit(1)
}
}

ZStack

除了 HStack 和 VStack 還有 ZStack, ZStack 的布局方式類似絕對定位, 在 UIKit 中類似 向一個 View 添加不同的 SubView。

VStack {
Text("Hello")
Text("World").padding(10);
}

VStack 的對齊方式組合自水平和垂直方向。

public struct Alignment : Equatable {

public var horizontal: HorizontalAlignment

public var vertical: VerticalAlignment

@inlinable public init(horizontal: HorizontalAlignment, vertical: VerticalAlignment)

public static let center: Alignment

public static let leading: Alignment

public static let trailing: Alignment

public static let top: Alignment

public static let bottom: Alignment

public static let topLeading: Alignment

public static let topTrailing: Alignment

public static let bottomLeading: Alignment

public static let bottomTrailing: Alignment

public static func == (a: Alignment, b: Alignment) -> Bool
}

Shape in SwiftUI

在 SwiftUI 中繪圖類似之前使用 UIBezierPath 的 API,只需要實現 Shape 的描述信息即可這裡就不多贅述了。

struct WedgeShape: Shape {
var wedge: Ring.Wedge

func path(in rect: CGRect) -> Path {
var p = Path()
let g = WedgeGeometry(wedge, in: rect)
p.addArc(center: g.cen, radius: g.r0, startAngle: g.a0, endAngle: g.a1, clockwise: false)
p.addLine(to: g.topRight)
p.addArc(center: g.cen, radius: g.r1, startAngle: g.a1, endAngle: g.a0, clockwise: true)
p.closeSubpath()
return p
}

}

SwiftUI 混合布局

很多現有的 APP 大量的使用 Cocoa 傳統的命令式 UI 布局方式,即便不用兼容 iOS 13.x 以下直接引入 SwiftUI,也是不可能全部改造為 SwiftUI,勢必會存在較長時間的混合編程。 蘋果已經考慮到這點,並且提供了非常簡單的方式供混合編程。

UIKit 嵌入 SwiftUI View

這個是最為常見的場景 SwiftUI 提供了 UI/NS/WKHostingController 包裝一個 SwiftUI View,並且提供了對於的視圖更新時機 setNeedsBodyUpdate updateBodyIfNeeded

SwiftUI View 嵌入 UIKit View

開發者已經封裝好了大量可用的視圖組件,可以直接嵌入到 SwiftUI 中無縫開發。

這塊就不做過多解釋了,這部分的代碼和時機分成清晰。 Demo 可以參考 Session 231 integrating swiftui(developer.apple.com/vid

SwiftUI On All Device

在前文 《新晉網紅SwiftUI——淘寶帶你初體驗》說到,蘋果考慮的跨平台不是像 RN Flutter Weex 的跨手機操作系統平台,而是跨 TV Watch Mac iPad 平台,所以理念也不一致,蘋果特意指出針對不同的設備,是沒有一種方法,能萬能的適配到所有設備上。 如果有勢必回抹掉很多平台特有的體驗,這雖然對開發者友好,但不見得對用戶是一件好事,蘋果指出了針對多設備的設計理念。

1、針對對應的平台選擇對應的合適的設計

2、認真分析共享模塊的設計

3、共享視圖是個有風險的行為,要考慮的更加深入

4、Learn once apply anyware.

在 SwiftUI 中開發者描述的視圖層只是數據的定義和描述, SwiftUI 框架內部已經做的足夠多,相信學習了 SwiftUI 可以輕易地在其他平台上開始開發。 關於更多細節請參考 Session 240 SwiftUI on all Device 和 Session 219 SwiftUI on watchOS(developer.apple.com/vid

accessibility in SwiftUI

今年 SwiftUI 也充分考慮了無障礙訪問功能,不得不說蘋果是非常有人性化的公司,SwiftUI 給無障礙開發減少了很多的工作量。不過作者對這方面了解甚少,就不誤導各位讀者了, 詳細的大家可以參考 Session 238 accessibility in swiftui。(developer.apple.com/vid

本文參考:

  1. 1、SE-0255 omit-return
  2. (https://github.com/apple/Swift-evolution/blob/master/proposals/0255-omit-return.md)
  3. 2、SE-0242 default-values-memberwise
  4. (https://github.com/apple/Swift-evolution/blob/master/proposals/0242-default-values-memberwise.md)
  5. 3、SE-0228 expressiblebystringinterpolation
  6. (https://github.com/apple/Swift-evolution/blob/master/proposals/0228-fix-expressiblebystringinterpolation.md)
  7. 4、SE-0258 property-wrappers
  8. (https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md)
  9. 5、function-builders.md
  10. (https://github.com/apple/swift-evolution/blob/9992cf3c11c2d5e0ea20bee98657d93902d5b174/proposals/XXXX-function-builders.md)
  11. 6、swift-evolution
  12. (https://github.com/apple/swift-evolution/tree/master/proposals)
  13. 7、Attribute大全
  14. (https://docs.swift.org/swift-book/ReferenceManual/Attributes.html)
  15. 8、Inside SwiftUIs Declarative Syntaxs Compiler Magic
  16. (https://swiftrocks.com/inside-swiftui-compiler-magic.html)
  17. 9、Session402
  18. (https://developer.apple.com/videos/play/wwdc2019/402/)
  19. 10、API Design GuideLine
  20. (https://swift.org/documentation/api-design-guidelines/)
  21. 11、新晉網紅SwiftUI——淘寶帶你初體驗
  22. 12、Session415
  23. (https://developer.apple.com/videos/play/wwdc2019/415)
  24. 13、聲明式編程
  25. (https://zh.wikipedia.org/wiki/%E5%AE%A3%E5%91%8A%E5%BC%8F%E7%B7%A8%E7%A8%8B)
  26. 14、命令式編程
  27. (https://zh.m.wikipedia.org/zh-my/%E6%8C%87%E4%BB%A4%E5%BC%8F%E7%B7%A8%E7%A8%8B)
  28. 15、understanding-property-wrappers-in-swiftui
  29. (https://mecid.github.io/2019/06/12/understanding-property-wrappers-in-swiftui/)

16、Session 402 Whats New in Swift (developer.apple.com/vid) 17、Session 204 Introducing SwiftUI: Building Your First App (developer.apple.com/vid) 18、Session 216 Introducing SwiftUI Essentials (developer.apple.com/vid) 19、Session 226 SwiftUI Essentials (developer.apple.com/vid) 20、Session 231 Integrating SwiftUI (developer.apple.com/vid) 21、Session 237 Building Custom Views with SwiftUI (developer.apple.com/vid) 22、Session 238 accessibility in swiftui (developer.apple.com/vid) 23、Session 240 SwiftUI on all Device (developer.apple.com/vid) 24、Session 219 SwiftUI on watchOS (developer.apple.com/vid) 25、Session 415 Modern Swift API Design (developer.apple.com/vid) 26、30 分鐘學會 Flex 布局 (zhuanlan.zhihu.com/p/25

本文作者:傾寒,來自淘寶客戶端iOS架構組

淘寶基礎平台團隊正在舉行2019實習生(2020年畢業)和社招招聘,崗位有iOS Android客戶端開發工程師、Java研發工程師、C/C++研發工程師、前端開發工程師、演算法工程師,歡迎投遞簡歷至[email protected] 如果你想更詳細了解淘寶基礎平台團隊,歡迎觀看團隊介紹視頻更多淘寶基礎平台團隊的技術分享,可關注淘寶技術微信公眾號AlibabaMTT


推薦閱讀:
相关文章