心得感悟
起初看到 WWDC 上的演示 SwiftUI 時,我就覺得 SwiftUI 有種陌生的熟悉感(聲明式語法),所以體驗下,看看有沒有什么啟發。
先說下整體項目完成下來的感受:
- 用 Swift + SwiftUI 開發 iOS 項目效率很高,本人之前沒有接觸過 Swift 語言,這次是從 0 開始學 swift 語言以及 swiftUI 框架的,每天花 2 個小時斷斷續續大致花了 3 天時間掌握了基本的 Swift 語法,而 SwiftUI 框架的掌握是按照官方的視頻學習的(贊下 Apple 的文檔、教學視頻的完備性);
- 從我一個前端開發工程師的視角看來,SwiftUI 使用的 “DSL”、狀態管理和前端的 React 很相似,不少概念是相通的,比如 State,Props。
- SwiftUI + Xcode11 + macOS Catalina 讓研發效率大大提升,預覽模式做到了所見即所得,注意這里是預覽模式而不是模擬器模式
- 借助於預覽模式,UI 也可以做到很好的單頁面的單元測試;這一點比較有意思,獨立的 View 可以獨立預覽,然后可以注入數據,實時預覽調試。
執行環境
- macOS Mojave: 10.14.5
- xcode: Version 11.0 beta 6 (11M392q)
項目信息
github: https://github.com/young-cowboy/swiftui-app-habits
App 效果預覽
2 個關鍵點
這里提 2 個關鍵點“尾部閉包語法”以及“狀態管理”,因為理解他們就基本能掌握 SwiftUI 來開發一個簡單的項目。尾部閉包語法能讓我們理解 SwiftUI 是如何通過聲明式語法來創建 View 的,“數據傳遞”能讓我們了解怎么實現數據流功能。
尾部閉包語法
Swift 有一個很重要的概念叫做“尾部閉包語法”,這種語法讓 Swift DSL 的書寫變得更“聲明式”,我們先了解下 Swift 閉包定義:
{(parameters) -> return type in
// 代碼
}
這樣來看是不是和 JavaScript 的閉包有一定的相似,但是閉包有很多種語法形式,這里我們舉個例子說明下:
import Foundation
func wrapClosure (closure: (String, String) -> Void) {
let name = "KK"
let age = "18"
closure(name, age)
}
定義一個 function,參數只有一個,並且這個參數為一個函數,然后在方法體內部調用這個函數,傳入參數;
func foo (name: String, age: String) {
print("My name is \(name) and \(age) years old")
}
wrapClosure(closure: foo) // My name is KK and 18 years old
通常我們會這樣執行,定義個函數來處理傳參。但是利用閉包的語法,有更簡潔的方式,如下就一個閉包的語法了
wrapClosure(closure: { (name: String, age: String) in
print("My name is \(name) and \(age) years old")
})
還可以利用 Swift 的類型推斷能力,不寫閉包參數的類型,如下
wrapClosure(closure: { name, age in
print("My name is \(name) and \(age) years old")
})
還可以利用快捷參數名來獲取參數,如下
wrapClosure(closure: {
print("My name is \($0) and \($1) years old")
})
最后還有更簡潔的方式,這樣是 SwiftUI 利用到的一個特性,所以SwiftUI 的 DSL 看起來很有聲明式的感覺,這個特性叫 “尾部閉包語法”,這次方法執行連括號都可以不寫了。
如果一個閉包是以一個函數的最后一個參數傳遞的,那么它就可以在函數的圓括號以外內聯。
wrapClosure {
print("My name is \($0) and \($1) years old")
}
到此應該能看出為什么 SwiftUI 能用聲明式的語法來創建各種 View 協議的視圖了。
數據傳遞
在做面向用戶的功能時,一個很主要的點是數據傳遞,不同的設計理念解決特點場景的問題。這里簡單介紹下 SwiftUI 的數據怎樣傳遞,這里有一個概念property wrapper
,我理解是裝飾器 decorate
,介紹三個裝飾器 @State
, @Binding
, @ObservedObject
,@Published
。
- @State 裝飾過的屬性發生了變化,SwiftUI 會根據新的屬性值重新創建視圖
- @Binding 修飾器修飾后,屬性變成了一個引用類型,傳遞變成了引用傳遞,這樣父子視圖的狀態就能關聯起
- @ObservedObject 修飾一個復雜類型數據,可以被多個 View 所使用,用 @Published 修飾對象里屬性,表示需要被監聽;
更多的細節可以參考:https://mecid.github.io/2019/06/12/understanding-property-wrappers-in-swiftui/
頁面結構設計
一共有 4 個頁面 HabitListView, AddButtonView, HabitDetailView, AddItemView
數據結構設計
項目涉及到三個數據結構: HabitItem(習慣項), HabitIconArray(習慣圖標), HabitColor(習慣主題)
HabitColor 主題色,保存了習慣的主題
public let UserColorArray = [
Color(red:75 / 255, green:166 / 255, blue: 239 / 255),
Color(red:161 / 255, green:206 / 255, blue: 97 / 255),
Color(red:248 / 255, green:214 / 255, blue: 80 / 255),
Color(red:243 / 255, green:176 / 255, blue: 74 / 255),
Color(red:238 / 255, green:140 / 255, blue: 111 / 255),
Color(red:237 / 255, green:113 / 255, blue: 165 / 255),
Color(red:207 / 255, green:102 / 255, blue: 247 / 255),
Color(red:77 / 255, green:110 / 255, blue: 247 / 255),
Color(red:236 / 255, green:107 / 255, blue: 102 / 255)
]
HabitIconArray 習慣的圖標庫
public let IconNameArray: [String] = [
"alarm",
"book",
"pencil",
"desktopcomputer",
"gamecontroller",
"sportscourt",
"lightbulb"
]
HabitItem 用來保存習慣的詳細信息,這里實現 ObservableObject 協議,用來告訴 SwiftUI 這個對象需要監聽,用 Published property wrapper 包裝了 checkList 屬性,表示這個屬性是要監聽的,因為它可能需要傳遞給子 View
class HabitItem: Identifiable, ObservableObject {
var name: String = ""
var iconName: String = "clock"
var theme: Color = UserColor.color1.value
var uuid: Int = 0
@Published var checkList: [Bool] = [false, false, false, false, false, false, false]
init () {
}
init(name: String, iconName: String, theme: Color) {
self.name = name
self.iconName = iconName
self.theme = theme
self.uuid = generatteID()
}
}
UI 設置
具體代碼參考倉庫代碼,這里講解一個流程,新建一個“習慣”項,在 MainView 里
MainView
...
@State var sheetVisible: Bool = false
@State var sheetType: String = "add"
...
...
AddButtonView() {
self.sheetType = "add"
self.sheetVisible = true
}
...
AddButtonView 利用尾部閉包語法
內聯了一個閉包用來相應事件
AddButtonView
里定義了 onPressed 屬性
struct AddButtonView: View {
var onPressed: () -> Void
var body: some View {
Button(action: {
self.onPressed()
}) {
HStack {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(Color.blue)
}
}
}
}
AddItemView
里的新增按鈕點擊后相應事件,把選中的數據傳遞 onSumit 屬性回調里
struct AddItemView: View {
@State var newItemTitle = ""
@State var selectIconIndex: Int = 0
@State var selectColorIndex: Int = 0
var onSumit: (HabitItem) -> Void
var onDissmis: () -> Void
var body: some View {
VStack {
...
...
VStack {
Button(action: {
if self.newItemTitle != "" {
let iconName = IconNameArray[self.selectIconIndex];
let theme = UserColorArray[self.selectColorIndex];
self.onSumit(HabitItem(name: self.newItemTitle, iconName: iconName, theme: theme))
}
}) {
Text("新增").frame(minWidth: 0, maxWidth: .infinity)
}
...
}
...
}
...
}
}
}
在 MainView 里,利用閉包處理回調事件新增選項
AddItemView(onSumit: { item in
self.habitItemList.insert(item, at: 0)
self.sheetVisible = false
}, onDissmis: { self.sheetVisible = false })
剩下的功能大同小異,可以把代碼拉下來本地運行看看效果,github: https://github.com/young-cowboy/swiftui-app-habits
謝謝