Lecture 4 | Stanford CS193p 2023
-
課程鏈接:https://www.youtube.com/watch?v=4CkEVfdqjLw
-
代碼倉庫:iOS
-
課程大綱:
簡要課程大綱:SwiftUI 高級主題
- Swift 訪問控制(Access Control)
- 5 個級別:
open
、public
、internal
、fileprivate
、private
private(set)
與fileprivate(set)
的用法- 在 SwiftUI 視圖與模塊化中的最佳實踐
- 5 個級別:
- 視圖初始化(
init
)與屬性包裝器配合- 合成
init
與自定義init
時機 - 在
init
中正確配置:@Binding
(父–子雙向綁定)@ObservedObject
(外部傳入模型)@StateObject
(首次創建模型)
init
中的限制與應將副作用延后到onAppear
- 合成
- 循環與遍歷 (
for-in
)- 遍歷數組、字典、范圍 (
Range
) enumerated()
獲取索引where
條件過濾、break
/continue
- 修改原集合的技巧
- 遍歷數組、字典、范圍 (
- 函數類型與閉包(Functions & Closures)
- 函數即類型:
() -> Void
、(Int) -> String
、() -> some View
- 回調參數與自定義
ViewBuilder
- 閉包語法簡寫:類型推斷、
$0
、省略return
- 捕獲(Capturing):閉包如何“包住”外部變量
- 函數即類型:
- 異步與逃逸閉包 (
@escaping
、async/await
)- 何時使用
@escaping
:網絡請求、GCD、定時器 - SwiftUI 中的異步:
Task { await … }
、.task
& 按鈕內部 - 結合
@MainActor
回到主線程更新狀態
- 何時使用
- 類型級成員:
static
變量與函數- 與實例無關的常量、工具方法
- 共享樣式、格式化器、預覽提供者 (
PreviewProvider
) struct
/enum
命名空間模式
- 值類型方法的
mutating
- 為什么值類型默認不可變
- 在模型層封裝可變邏輯:
mutating func
- SwiftUI 中通過
@State
/@Binding
替代直接使用
- 語義化重命名(Semantic Rename)
- Xcode Refactor → Rename 操作
- 跨文件、跨模塊安全重命名接口/類型
- 保持項目代碼一致性
- SwiftUI 響應式 UI 與狀態管理
- 單一真相 與 聲明式+響應式 流程
- 狀態改變(
@State
、@Published
、環境值) - Combine Publisher 發事件
- SwiftUI 標記 View 失效 → 重算
body
→ diff → 渲染
- 狀態改變(
- 屬性包裝器詳解:
@State
:局部輕量狀態@Binding
:父–子雙向綁定@StateObject
:首次創建并擁有的模型@ObservedObject
:外部傳入并訂閱的模型@EnvironmentObject
:跨層級共享模型@Environment
: 系統/自定義環境值
- 典型場景示例與對比
- 單一真相 與 聲明式+響應式 流程
- Swift 訪問控制(Access Control)
文章目錄
- Lecture 4 | Stanford CS193p 2023
- 1. access control
- 1.1 疊加用法:
- 1.2 細節:
- 2.init
- 2.1 默認合成的 init 與何時需要自定義
- 2.2 與屬性包裝器配合
- 2.3`init` 中的限制與行為
- 3. for in
- 3.1 基本語法
- 3.1 遍歷常見類型
- 3.1.1 遍歷數組
- 3.1.2 遍歷區間(范圍)
- 3.1.3 遍歷字典
- 3.1.4 遍歷并獲取索引(`enumerated()`)
- 3.2 控制語句搭配
- 3.2.1 使用 `where` 添加條件
- 3.2.2 使用 `break` 和 `continue`
- 3.2.3 注意事項
- 3.2.4 小結表格
- 4. functions as types
- 4.1 函數作為類型的幾種場景
- 4.1.1 **回調函數傳參**
- 4.1.2 **作為 ViewBuilder 的函數參數**
- 4.1.3 **事件處理(onTap、gesture)**
- 4.1.4 函數類型的形式
- 4.1.5 SwiftUI 中 View 本質上也是函數調用鏈
- 4.1.6 總結要點
- 5.閉包
- 5.1 什么是閉包(Closure)?
- 5.2 閉包的基本語法
- 5.2.1 帶參數的閉包
- 5.2.2 閉包 vs 函數
- 5.3 閉包的常見應用(SwiftUI 初學者常見場景)
- 5.3.1 **事件回調**
- 5.3.2 **傳入函數中**
- 5.3.3 **構建 UI 視圖(@ViewBuilder)**
- 5.4 Swift 閉包的簡寫語法
- 5.5 閉包是如何“捕獲值”的?
- 5.6 閉包 + 狀態(@State / @Binding)的常見模式
- 5.7`@escaping` 和異步閉包在 SwiftUI 中的角色
- 6.static vars and func
- 6.1 `static` 屬性(靜態變量/常量)
- 6.2 `static` 方法(靜態函數)
- 6.3 與 `class` 的區別
- 6.4 SwiftUI 特殊用例:預覽提供者
- 6.5 使用建議
- 7.semantic rename
- 8.mutating
- 8.1為什么需要 `mutating`
- 8.2 在 SwiftUI 中的典型用法
- 8.3 何時在 SwiftUI 里真正用到 `mutating`
- 8.4 小結
- 9.Swift中的狀態管理
- 9.1 核心理念
- 9.2 屬性包裝器詳解
- 1. @State
- 2. @Binding
- 3. @StateObject
- 4. @ObservedObject
- 5. @EnvironmentObject
- 6. @Environment
- 9.3 完整數據流動流程
- 9.4 選用指南與實踐
1. access control
在 SwiftUI 中,訪問控制(Access Control)沿用了 Swift 語言本身的五個級別:open、public、internal(默認)、fileprivate、private。合理運用這些修飾符,能有效隔離視圖接口與內部實現,增強模塊化與可維護性。以下分幾部分詳述其在 SwiftUI 開發中的常見應用和注意事項。
級別 | 模塊外可見 | 同一模塊內可見 | 同一文件內可見 | 同一類型內可見 |
---|---|---|---|---|
open | ? | ? | ? | ? |
public | ? | ? | ? | ? |
internal | ? | ? | ? | ? |
fileprivate | ? | ? | ? | ? |
private | ? | ? | ? | ? |
- open/public:導出的 API,可被外部模塊導入與調用;只有 open 允許被子類化與重寫。
- internal(默認):僅限當前模塊(App 或 Framework)內部可見。
- fileprivate:僅限同一源文件內可見。
- private:僅限同一聲明域(類型或擴展)內可見。
1.1 疊加用法:
Swift 目前只支持將 setter 訪問權限 狹窄化到 fileprivate 或 private,也就是只有這兩種寫法,你可以把它們和任何更寬松的訪問級別(open、public、internal、fileprivate)配合使用,但不能反過來擴大:
// —— 合法:只能比聲明級別更“私有” ——// 1?? 完全私有:同類型(同擴展/同花括號)內可寫,其它地方只讀
private(set) var foo: Int // 2?? 文件私有:同文件內可寫,其它文件只讀
fileprivate(set) var bar: Int // 3?? 公共只讀:模塊外可讀,模塊/文件內可讀,只有類型內部可寫
public private(set) var baz: Int // 4?? 開放只讀(rare):模塊外可繼承/可重寫,外部可讀,只有類型內私有寫
open private(set) var qux: Int // 5?? 模塊內只寫:顯式寫 internal private(set),等同默認 internal+private(set)
internal private(set) var quux: Int
不能寫成 public(set)、internal(set)、open(set) 之類的——編譯器只允許你用 fileprivate(set) 或 private(set)。
1.2 細節:
1?? open和public的區別:
- open = 公共可訪問 + 外部可繼承/可重寫
- public = 僅公共可訪問,外部不可繼承/不可重寫
選擇時,牢記“最小暴露原則”:能用 public 限制就別用 open,避免無意中開放過多擴展點。另外這里可以看到差異主要在繼承性上,所以open只能修飾可繼承的類型!
2?? 視圖類型(struct View)的可見性:
- 默認 internal:如果你不標注,SwiftUI 視圖對同一模塊內都可見;通常足夠應用內部模塊化。
2.init
在 SwiftUI 中,所有視圖(View)本質上都是值類型(struct),它們的初始化器(init)承擔著以下核心職責:
- 接收外部參數并初始化存儲屬性
- 配置屬性包裝器(@State、@Binding、@ObservedObject、@StateObject 等)
- 決定視圖的初始狀態
下面從幾個角度詳細說明 SwiftUI 中的 init 使用要點。
2.1 默認合成的 init 與何時需要自定義
- 合成初始化器:
如果你的struct MyView: View
中所有存儲屬性都有默認值,且你沒有顯式定義任何init
,Swift 會自動合成一個無參init()
。 - 需要自定義的場景:
視圖需要接受參數,例如:
struct GreetingView: View {let name: Stringvar body: some View { Text("Hello, \(name)!") }
}
// Swift 會合成: init(name: String)
2.2 與屬性包裝器配合
@State、@Binding、@ObservedObject、@StateObject
[!NOTE]
注意:SwiftUI 中的這些屬性包裝器都遵從 DynamicProperty,init 中的賦值通常要用底層存儲屬性(帶下劃線的形式)。
包裝器 | 初始化要求 |
---|---|
@State | 可給出初始值:@State private var count = 0 ,無需在 init 中處理 |
@Binding | 必須由父視圖傳入:init(isOn: Binding<Bool>) { _isOn = isOn } |
@ObservedObject | 通常由外部傳入已有的 ObservableObject:init(viewModel: VM) { _viewModel = ObservedObject(wrappedValue: viewModel) } |
@StateObject | 只在視圖生命周期內首次創建:init() { _vm = StateObject(wrappedValue: VM()) } 或通過參數注入 |
@EnvironmentObject / @Environment | 由系統注入,不需手動初始化 |
例如:
class VM: ObservableObject {@Published var name = "World"
}struct DetailView: View {// 父視圖或外部注入@ObservedObject private var viewModel: VM// 或首次創建@StateObject private var createdVM: VM// custom init 必須配置包裝器的底層存儲init(viewModel: VM) {// 注意左側下劃線:訪問包裝器的底層存儲_viewModel = ObservedObject(wrappedValue: viewModel)_createdVM = StateObject(wrappedValue: VM())}var body: some View {VStack {Text("Observed: \(viewModel.name)")Text("Created: \(createdVM.name)")}}
}
@Binding
示例:
struct ToggleView: View {@Binding private var isOn: Boolinit(isOn: Binding<Bool>) {_isOn = isOn}var body: some View {Toggle("Switch", isOn: $isOn)}
}
2.3init
中的限制與行為
body
尚不可用
init
執行時body
尚未構建,不要在init
里觸發視圖渲染或依賴body
屬性。- 禁止在
init
中做副作用
避免在init
中執行網絡請求、定時器啟動等副作用;應把這類邏輯放在onAppear
或視圖模型里。 @StateObject
首次初始化
-
@StateObject
只能在視圖的init
中賦予初始值一次;后續視圖重建時(如父視圖刷新)不會重新初始化。 -
切記不要在視圖的其它生命周期方法(如
body
)中再次創建StateObject
,以免丟失狀態。
3. for in
3.1 基本語法
for item in collection {// 執行操作
}//或者我們在循環中并不關心索引,只是想循環若干次,那么就使用_來代替
for _ in 1..<5 {// 執行操作
}
3.1 遍歷常見類型
3.1.1 遍歷數組
let fruits = ["Apple", "Banana", "Cherry"]for fruit in fruits {print("I like \(fruit)")
}
3.1.2 遍歷區間(范圍)
for i in 1...5 {print(i) // 輸出 1 到 5(閉區間)
}for i in 1..<5 {print(i) // 輸出 1 到 4(半開區間)
}
[!NOTE]
關于范圍:
Swift 中的
1...n
這種寫法,也叫區間運算符(Range Operator),在for-in
循環中經常使用。
1...n
:閉區間運算符(Closed Range),表示[1, n]1..<n
:半開區間運算符(Half-Open Range),表示[1, n)- 逆序遍歷:
(1...5).reversed()
3.1.3 遍歷字典
let scores = ["Alice": 90, "Bob": 85]//有點類似于C++17中的結構化綁定的寫法,只不過C++用的是[]
for (name, score) in scores {print("\(name): \(score)")
}
3.1.4 遍歷并獲取索引(enumerated()
)
let names = ["Tom", "Jerry", "Spike"]for (index, name) in names.enumerated() {print("\(index): \(name)")
}
3.2 控制語句搭配
3.2.1 使用 where
添加條件
for i in 1...10 where i % 2 == 0 {print(i) // 輸出偶數
}
3.2.2 使用 break
和 continue
for i in 1...5 {if i == 3 { continue } // 跳過3if i == 5 { break } // 提前終止print(i)
}
3.2.3 注意事項
-
循環變量默認為常量(
let
),不可修改:for n in numbers {n += 1 // ? 編譯錯誤 }
-
如需修改原數組,建議使用下標訪問:
for i in 0..<array.count {array[i] += 1 }
3.2.4 小結表格
類型 | 示例 | 說明 |
---|---|---|
數組 | for x in arr | 遍歷元素 |
區間 | for i in 1...n | 閉/開區間 |
字典 | for (k, v) in dict | 遍歷鍵值對 |
索引+值 | for (i, v) in arr.enumerated() | 同時獲取索引和值 |
條件遍歷 | for i in 1...10 where i % 2 == 0 | 添加篩選條件 |
4. functions as types
在 SwiftUI 中,“functions as types” 是一個很重要的概念,尤其是在寫 ViewBuilder
、事件回調(例如 .onTapGesture
)或自定義組件時。它體現的是 Swift 的**一等函數(first-class functions)**特性 —— 也就是說,函數本身就是一種類型,可以作為值傳遞、賦值、返回或存儲。
🧠 一句話理解
在 SwiftUI 中,函數不僅能“被調用”,還能“被傳遞”或“存儲”為類型使用。
4.1 函數作為類型的幾種場景
4.1.1 回調函數傳參
struct MyButton: View {let action: () -> Void // 函數作為類型(無參數、無返回)var body: some View {Button("Tap Me", action: action)}
}// 使用方式
MyButton {print("Button tapped!")
}
() -> Void
是一個函數類型,表示無參無返回值。- 可以把函數當作變量一樣傳進視圖中。
4.1.2 作為 ViewBuilder 的函數參數
struct CardView<Content: View>: View {let content: () -> Content // 函數類型:返回一個 Viewvar body: some View {VStack {Text("Title")content() // 調用函數,插入子視圖}}
}// 使用
CardView {Text("Hello")
}
這就是 SwiftUI 的聲明式 UI:子視圖就是一個函數返回的 View 類型!
4.1.3 事件處理(onTap、gesture)
Text("Tap me").onTapGesture(perform: {print("Tapped")})
- 這里的
perform:
參數是一個() -> Void
函數。 - 你可以傳匿名函數(閉包),也可以傳已有函數名。
4.1.4 函數類型的形式
函數簽名 | 意義 |
---|---|
() -> Void | 無參無返回 |
(Int) -> String | 傳入 Int,返回 String |
() -> some View | 返回一個 SwiftUI 視圖 |
@escaping () -> Void | 函數逃逸,用于異步回調 |
4.1.5 SwiftUI 中 View 本質上也是函數調用鏈
SwiftUI 中寫:
Text("Hi").foregroundColor(.red)
本質是函數組合:
func foregroundColor(_ color: Color) -> some View
所以整個 .modifier(...)
等鏈式調用,都依賴于“函數作為類型”這一底層支持。
4.1.6 總結要點
- Swift 中函數是一等類型,可以像變量一樣使用。
- SwiftUI 中大量用到
() -> View
類型構建視圖樹。 - 事件、回調、聲明式組件傳遞都依賴于“函數類型”。
- 泛型和
@ViewBuilder
常用于約束這類函數。
5.閉包
5.1 什么是閉包(Closure)?
? 通俗解釋:
閉包就是“一段可以被當作變量使用的函數代碼”。
你可以像“值”一樣,把它傳給別人、存起來,或者作為參數傳入另一個函數中。
🧩 為什么叫“閉包”?
閉包這個名字來自于它**“捕獲”并“記住”其作用域內的變量** —— 就像一個函數“包住”了它定義時的上下文。
5.2 閉包的基本語法
let greet = {print("Hello")
}greet() // 調用閉包,輸出:Hello
{ ... }
是閉包體,和函數體很像。greet
是閉包變量,類型是() -> Void
,表示“無參無返回值的函數”。
5.2.1 帶參數的閉包
let square = { (x: Int) -> Int inreturn x * x
}let result = square(5) // 輸出 25
(x: Int) -> Int
是閉包類型。in
把參數和閉包體分開。
5.2.2 閉包 vs 函數
項目 | 閉包 | 函數 |
---|---|---|
語法 | { (param) -> Ret in ... } | func name(param) -> Ret {} |
用途 | 傳值、回調、構建視圖等 | 定義具體邏輯單元 |
是否有名字 | 一般沒有(也可以有) | 一定有名字 |
5.3 閉包的常見應用(SwiftUI 初學者常見場景)
5.3.1 事件回調
Button("Tap me") {print("Button clicked!") // 這個就是閉包
}
5.3.2 傳入函數中
func performTwice(action: () -> Void) {action()action()
}performTwice {print("Doing it twice")
}
5.3.3 構建 UI 視圖(@ViewBuilder)
VStack {Text("Line 1")Text("Line 2")
}
其實 VStack {}
括號中的內容就是一個返回 View
的閉包。
? 小結
- 閉包 = 可以當作變量傳來傳去的“函數代碼塊”
- 語法:
{ (參數) -> 返回類型 in 代碼 }
- SwiftUI 到處都在用閉包,比如構建 UI、處理按鈕點擊、響應變化等等
5.4 Swift 閉包的簡寫語法
Swift 提供了非常強大的閉包語法簡化能力,常見于 SwiftUI、排序、過濾等場景。
? 簡寫 1:省略返回類型(類型可推斷)
let double: (Int) -> Int = { x inreturn x * 2
}
? 簡寫 2:省略參數名,用 $0
、$1
等表示
let double: (Int) -> Int = { $0 * 2 }let sum: (Int, Int) -> Int = { $0 + $1 }print(double(3)) // 輸出 6
print(sum(3, 4)) // 輸出 7
[!NOTE]
這里看到
return
也被省略了,原因是閉包只有一個表達式,Swift 編譯器就自動將那個表達式作為返回值。
🧪 應用例子:數組 map / filter
let numbers = [1, 2, 3]// 用閉包把每個數 *2
let doubled = numbers.map { $0 * 2 }
print(doubled) // [2, 4, 6]// 過濾出大于1的數
let filtered = numbers.filter { $0 > 1 }
print(filtered) // [2, 3]
map {}
和filter {}
都接受閉包作為參數。$0
是當前遍歷的元素。
? 小結:閉包簡寫語法順序
步驟 | 示例 | 說明 |
---|---|---|
完整寫法 | { (x: Int) -> Int in ... } | 明確參數和返回類型 |
推斷類型 | { x in ... } | 參數類型被類型系統推斷 |
使用簡寫參數名 | { $0 + 1 } | 用 $0 表示第一個參數 |
單表達式 | { $0 + 1 } 無需 return | Swift 自動返回表達式結果 |
5.5 閉包是如何“捕獲值”的?
? 一句話理解:
Swift 的閉包可以“記住”它創建時上下文中的變量值,即使這些變量的作用域已經消失。
這就是閉包名字的由來:“閉”住了上下文,“包”住了變量。
🧪 示例一:最經典的捕獲行為
func makeCounter() -> () -> Int {var count = 0let counter = {count += 1return count}return counter
}let c = makeCounter()print(c()) // 1
print(c()) // 2
print(c()) // 3
[!NOTE]
count
是makeCounter
函數里的局部變量。- 閉包
{ count += 1 }
把它“捕獲”了,即使函數早就返回了,count 依然存在并可訪問。- 每次調用
c()
都在修改它“私有”的那份count
。這就叫閉包捕獲(Closure Capturing)。
🧪 示例二:多個閉包共享同一個上下文變量
func makeTwoCounters() -> (() -> Int, () -> Int) {var count = 0let increment = { () -> Int incount += 1return count}let report = { () -> Int inreturn count}return (increment, report)
}let (inc, read) = makeTwoCounters()
print(inc()) // 1
print(inc()) // 2
print(read()) // 2 (共享變量)
兩個閉包 捕獲了同一個變量,它們共享狀態!
? 閉包捕獲變量的特點
特性 | 說明 |
---|---|
持久性 | 被捕獲的變量不會因為函數返回而銷毀 |
引用語義 | 閉包對變量是“引用”而不是“拷貝”(除非顯式處理) |
多閉包共享變量 | 多個閉包可共享同一捕獲的變量 |
📌 Swift 中常見的閉包捕獲用法
- 計數器(如上例)
- 異步回調(需要捕獲某些狀態)
- SwiftUI 的動畫或響應事件回調
- GCD、Timer、URLSession 中使用 self 時注意捕獲方式
? 小結
- 閉包“包住”它創建時的變量環境,函數作用域結束后也能繼續訪問。
- 這是閉包最大的特性之一。
- 被捕獲的變量其實是“引用捕獲”,會被閉包持有。
5.6 閉包 + 狀態(@State / @Binding)的常見模式
SwiftUI 中的視圖是“聲明式的 + 響應式的”,狀態改變會自動觸發 UI 更新。而閉包,正是負責驅動狀態改變、處理用戶操作的關鍵。
🎯 目標例子:計數器按鈕
struct CounterView: View {@State private var count = 0var body: some View {VStack {Text("Count: \(count)").font(.largeTitle)Button("Tap Me") {// 這個閉包被觸發后,count 狀態會更新,UI 自動刷新count += 1}}}
}
[!NOTE]
? 分析:
@State
是一個源狀態,count
是 UI 的數據來源。Button {}
中的閉包負責更改狀態。- SwiftUI 自動追蹤這個狀態,狀態變 → UI 自動變。
🔁 模式 1:閉包響應狀態更新
🧪 示例:切換開關
@State private var isOn = falseToggle("Enable feature", isOn: $isOn)
//$isOn 是綁定(Binding<Bool>),它把對 isOn 的讀寫操作封裝起來,傳給 Toggle 控件。
//當開關切換時,Toggle 會通過這個 Binding 自動更新 isOn。
//當 isOn 變化時,界面也會自動刷新。
- 這個
Toggle
會自動綁定isOn
狀態。 - 你也可以加入閉包響應狀態變化:
Toggle("Enable", isOn: $isOn).onChange(of: isOn) { newValue inprint("Switch changed: \(newValue)")}
onChange
接收一個閉包(T) -> Void
,在狀態變更時調用。
🔄 模式 2:父子組件通信用閉包 + @Binding
? 目標:點擊子視圖按鈕,讓父視圖的計數器增加
🔧 子視圖:
struct ChildView: View {let onTap: () -> Void // 閉包作為參數var body: some View {Button("Child Tap", action: onTap)}
}
🧩 父視圖:
struct ParentView: View {@State private var count = 0var body: some View {VStack {Text("Count: \(count)")ChildView {count += 1 // 閉包捕獲父視圖狀態}}}
}
🧠 總結:
- 子視圖通過閉包
onTap
通知父視圖。 - 父視圖通過
@State
持有狀態并在閉包中修改它。 - 這是 SwiftUI 單向數據流 + 閉包回調機制的體現。
📦 模式 3:@Binding + 閉包做表單交互組件
struct LabeledToggle: View {@Binding var isOn: Bool // 由父視圖提供狀態var body: some View {Toggle("Enabled", isOn: $isOn)}
}
在父視圖中這樣使用:
@State private var active = falseLabeledToggle(isOn: $active)
- 這里沒有顯式閉包,但其實**
$isOn
就是一個雙向綁定的“狀態驅動型閉包”**。 - 你可以想象它像這樣工作:
Toggle("...", isOn: Binding(get: { active },set: { active = $0 }
))
? 小結:閉包 + 狀態模式對照表
場景 | 使用方式 | 本質 |
---|---|---|
點擊按鈕更新狀態 | Button { count += 1 } | 閉包捕獲 @State |
狀態變更響應 | .onChange(of: var) { ... } | 閉包監聽狀態 |
子傳父回調 | ChildView(onTap: { ... }) | 閉包回調 + 狀態驅動 |
組件綁定 | @Binding var isOn + $value | 雙向狀態驅動 |
好的,我們繼續進入 SwiftUI 中閉包學習的第 5 部分,這部分將引入一個非常重要但常被忽略的實踐主題:
5.7@escaping
和異步閉包在 SwiftUI 中的角色
? 一句話理解
在 SwiftUI 中,所有延遲執行、異步觸發或持久保存的閉包都必須標注為
@escaping
,這是確保閉包在作用域外仍然有效的關鍵。
而 SwiftUI + async/await 的結合,也需要閉包支持異步結構。
🔍 回顧:什么是 @escaping
在 Swift 中,默認閉包是 非逃逸(non-escaping) —— 也就是說必須在函數體內被調用完。
而 @escaping
表示:這個閉包可能在函數返回之后才會被調用。
場景 1:異步請求(如網絡請求)
func loadData(completion: @escaping (String) -> Void) {DispatchQueue.global().async {// 模擬異步任務sleep(1)DispatchQueue.main.async {completion("Loaded result")}}
}
調用:
loadData { result inprint("Result is \(result)")
}
- 閉包作為回調函數,要等異步操作完成后再調用,所以必須是
@escaping
。
場景 2:SwiftUI 中異步任務配合閉包更新狀態
struct AsyncExample: View {@State private var message = "Loading..."var body: some View {VStack {Text(message)Button("Load") {loadData { result inmessage = result}}}}
}
[!NOTE]
loadData
是一個接受@escaping
閉包的異步函數。- SwiftUI 中的
@State
被閉包捕獲后,更新狀態會自動刷新 UI。
? 進階:SwiftUI + async/await
從 Swift 5.5 開始,你可以用 async
和 await
寫更清晰的異步邏輯,配合 SwiftUI 的 .task
或 Button
:
🔧 示例:
struct AsyncAwaitExample: View {@State private var message = "Loading..."var body: some View {VStack {Text(message)Button("Fetch") {Task {message = await fetchData()}}}}func fetchData() async -> String {try? await Task.sleep(nanoseconds: 1_000_000_000)return "Async result"}
}
[!NOTE]
Task {}
是一個自動逃逸的異步環境(相當于 GCD)。- 閉包內部可以寫
await
。- 你無需顯式寫
@escaping
,因為Task
本身持有閉包。
? 閉包 + @escaping + @MainActor 常見組合
func asyncWork(completion: @escaping (String) -> Void) {Task {let result = await fetchData()await MainActor.run {completion(result)}}
}
- 在后臺執行異步任務
- 回到主線程通過
@MainActor
調用閉包更新 UI
? 小結
場景 | 是否需要 @escaping | 示例 |
---|---|---|
異步請求回調 | ? 是 | completion: @escaping () -> Void |
SwiftUI 的 Button | ? 否(立即調用) | Button {} |
Task 內異步閉包 | ? 自動逃逸 | Task { await ... } |
網絡或后臺任務 | ? 是 | URLSession , DispatchQueue 等 |
[!NOTE]
- 在 SwiftUI 中,大多數閉包是非逃逸的,除非你進入異步、后臺、回調等場景。
- 使用
@escaping
的函數通常與你的狀態更新有關,所以要注意閉包捕獲@State
或@Binding
的方式,避免內存泄漏。
6.static vars and func
在 Swift(包括 SwiftUI)中,用 static
修飾的屬性和方法都是“類型級”(type-level)的,而不是“實例級”(instance-level)的。它們的主要特點和常見用法包括:
6.1 static
屬性(靜態變量/常量)
-
定義方式
struct ContentView: View {// 存儲型靜態常量static let defaultTitle: String = "歡迎"// 計算型靜態屬性static var defaultColor: Color {return .blue.opacity(0.8)}var body: some View {Text(Self.defaultTitle).foregroundColor(Self.defaultColor)} }
-
特點
- 與實例無關:不需要創建
ContentView()
實例,就可以直接通過ContentView.defaultTitle
訪問。 - 共享:在所有實例中只有一份存儲或計算邏輯,可用于緩存重用,比如
NumberFormatter
、DateFormatter
、自定義樣式等。 - 延遲初始化:存儲型
static
屬性在首次訪問時才會創建(thread-safe)。
- 與實例無關:不需要創建
6.2 static
方法(靜態函數)
-
定義方式
struct ContentView: View {static func greeting(for name: String) -> String {"Hello, \(name)!"}var body: some View {Text(Self.greeting(for: "SwiftUI"))} }
-
用途
- 編寫與實例無關的“工具函數”或“工廠方法”。
- 在
View
中作為輔助邏輯,避免在body
中出現復雜計算。
6.3 與 class
的區別
static
只能用于值類型(如struct
、enum
)或class
的“不可重寫”成員。class
方法或屬性可以在子類中用override
重寫;而static
則不允許重寫。
6.4 SwiftUI 特殊用例:預覽提供者
struct ContentView_Previews: PreviewProvider {// SwiftUI 要求:必須是 static varstatic var previews: some View {ContentView().previewLayout(.sizeThatFits)}
}
PreviewProvider
協議要求提供一個static var previews
,用來在 Xcode 畫布中渲染多組預覽。
6.5 使用建議
- 常量、共享資源:將不隨實例變化的配置、樣式、Formatter 等放進
static let
。 - 輔助函數:與視圖實例狀態無關的純函數可聲明為
static func
。 - 命名空間:可利用
struct
+static
對一組相關常量/方法進行邏輯分組。
7.semantic rename
在Xcode中我們如果想對一個接口或者類型進行修改命名的話,如果我們直接手動修改會比較麻煩,并且會導致修改錯誤;
這時候我們就可以借Xcode提供的修改接口來完成我們的修改操作:
- Step1:選擇你要修改類型名右鍵之后選擇refactor->rename
- Step2: 在輸入框里寫入修改后的名稱,然后點擊rename 就好了!
8.mutating
在 Swift 里,mutating
是一個修飾符,用在值類型(struct
或 enum
)的方法前,表示這個方法會修改它自身(self
)或它的屬性。因為值類型默認方法不能改變自己(以保證值語義),加上 mutating
后,編譯器才允許你在方法里寫諸如 self.count += 1
之類的操作。
8.1為什么需要 mutating
-
值類型不可變性
默認情況下,struct
/enum
的方法里不允許修改它們的存儲屬性:struct Point {var x: Double, y: Doublefunc moveBy(dx: Double, dy: Double) {// x += dx // ? 編譯錯誤:Cannot assign to property: 'self' is immutable} }
-
加上
mutating
后就行了struct Point {var x: Double, y: Doublemutating func moveBy(dx: Double, dy: Double) {x += dx // ?y += dy} }
8.2 在 SwiftUI 中的典型用法
雖然 SwiftUI 的 View
本身也是一個 struct
,但你幾乎不會在自定義的 View
上直接寫 mutating func
;而是通過屬性包裝器(如 @State
、@Binding
)來管理可變狀態。比如:
struct CounterView: View {// 這是一個引用類型的“盒子”,底層幫你做了 mutating@State private var count = 0var body: some View {VStack {Text("Count: \(count)")Button("Increment") {count += 1 // 不用自己寫 mutating}}}
}
如果你要在 獨立的模型類型(非 View)里封裝修改邏輯,就要用 mutating
:
// 只是一個純值類型模型,它會被 @State 或 @ObservedObject 持有
struct Counter {var value = 0mutating func increment() {value += 1}
}struct CounterView: View {@State private var counter = Counter()var body: some View {VStack {Text("Value: \(counter.value)")Button("+") {counter.increment() // 調用的是 mutating 方法}}}
}
8.3 何時在 SwiftUI 里真正用到 mutating
- 自定義業務模型(純 Swift 結構體)
- 將可變邏輯封裝在模型內部,用
mutating
標記。 - 通過
@State
、@Binding
或@ObservedObject
在 View 中引用模型實例。
- 將可變邏輯封裝在模型內部,用
- 擴展值類型
- 例如給自定義
Shape
、Layout
、PreferenceKey
等結構體添加修改自身狀態的方法。
- 例如給自定義
- 協議實現里需要修改自身
- 某些協議(如自定義的協議)要求方法能改變結構體屬性,就要在方法簽名前加
mutating
。
- 某些協議(如自定義的協議)要求方法能改變結構體屬性,就要在方法簽名前加
8.4 小結
- Swift 語言層面:
mutating
讓值類型方法能夠改變self
。 - SwiftUI 層面:大多數狀態變化都是通過屬性包裝器來實現,你很少在
View
上直接寫mutating
。 - 最佳實踐:如果模型本身是值類型,而且你想把修改邏輯封裝進去,別忘了在方法前加
mutating
;在 View 里就直接調用模型方法或操作@State
/@Binding
即可。
9.Swift中的狀態管理
9.1 核心理念
- 聲明式+響應式:UI 聲明“我想展示什么”,狀態改變后自動“重繪”界面。
- 單一真相(Single Source of Truth):狀態(State)是唯一可靠的數據源,所有 UI 都從它派生。
- 響應式更新流程:
- 狀態改變(@State、@Published…)
- Publisher 發事件
- SwiftUI 標記失效
- 重新執行
body
- diff & 渲染(只是更新必要部分)
9.2 屬性包裝器詳解
下面以定義 → 具體代碼示例 → 數據流通 → 應用場景四步來展開講解。
1. @State
-
定義:在單個 View 內部管理私有、可變的值類型狀態。
-
示例:
struct CounterView: View {@State private var count: Int = 0var body: some View {VStack {Text("Count: \(count)")Button("+1") { count += 1 }}.padding()} }
-
數據流通:
count += 1
→ 寫入內部“狀態槽”@State
底層是 Combine Publisher,發出新值事件- SwiftUI 標記該 View 失效 → 下一幀調用
body
- diff 新舊視圖 → 最小化更新
-
應用場景:
- 局部、輕量:開關、計數器、TextField 文本內容等,僅限當前 View 使用的狀態。
2. @Binding
-
定義:在父–子 View 間建立雙向引用,子 View 可以讀寫父 View 的
@State
。 -
示例:
// 父 View struct ParentView: View {@State private var isOn = falsevar body: some View {ToggleView(isOn: $isOn)} }// 子 View struct ToggleView: View {@Binding var isOn: Boolvar body: some View {Toggle("開關", isOn: $isOn).padding()} }
-
數據流通:
- 子 View 調用
isOn.toggle()
- 實際修改父 View 的
@State isOn
→ 觸發@State
流程 - 父 View 重算
body
,通過$isOn
傳回最新值給子 View
- 子 View 調用
-
應用場景:
- 組件化:當你拆分 View,希望子組件既能讀取又能修改父組件的狀態時。
3. @StateObject
-
定義:用于在 View 首次創建時初始化并持有一個
ObservableObject
,負責其生命周期。 -
示例:
final class TimerModel: ObservableObject {@Published var seconds = 0private var timer: Timer?init() {timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ inself.seconds += 1}} }struct TimerView: View {@StateObject private var model = TimerModel()var body: some View {Text("Time: \(model.seconds)s").font(.largeTitle)} }
-
數據流通:
model.seconds += 1
→@Published
發事件- SwiftUI 捕獲事件 → 標記
TimerView
失效 - 重算
body
→ 更新顯示
-
應用場景:
- View 自有的復雜狀態,如網絡請求結果、定時器、音視頻播放器等只由該 View 管理的對象。
4. @ObservedObject
-
定義:用于訂閱外部傳入的
ObservableObject
,監聽其@Published
屬性。 -
示例:
final class Settings: ObservableObject {@Published var username: String = "" }struct ProfileView: View {@ObservedObject var settings: Settingsvar body: some View {VStack {TextField("用戶名", text: $settings.username).textFieldStyle(RoundedBorderTextFieldStyle())Text("Hello, \(settings.username)")}.padding()} }// 使用時由父 View 或環境傳入 struct Container: View {@StateObject private var settings = Settings()var body: some View { ProfileView(settings: settings) } }
-
數據流通:
- 外部某處
settings.username = "新名"
→@Published
發事件 - SwiftUI 標記
ProfileView
失效 → 重算body
→ 更新界面
- 外部某處
-
應用場景:
- 共享狀態:多個子 View 需要訂閱同一個模型,但模型的生命周期由外部管理時。
5. @EnvironmentObject
-
定義:在環境中全局注入并共享的
ObservableObject
,可跨多層 View 訪問。 -
示例:
@main struct MyApp: App {@StateObject private var userData = UserData()var body: some Scene {WindowGroup {ContentView().environmentObject(userData)}} }struct ContentView: View {var body: some View {VStack {ProfileView() // 及其子 View 均能訪問 userDataDashboardView()}} }struct ProfileView: View {@EnvironmentObject var userData: UserDatavar body: some View { Text("User: \(userData.name)") } }
-
數據流通:
- 任意層級調用
userData.name = "新名"
→@Published
發事件 - 所有訂閱該對象的 View 都失效 → 各自重算
body
→ 更新
- 任意層級調用
-
應用場景:
- 跨模塊共享:用戶設置、全局主題、購物車數據等需要在 App 多處訪問的全局狀態。
6. @Environment
-
定義:讀取系統或自定義的環境值(如配色方案、字體、布局方向等)。
-
示例:
struct ThemedView: View {@Environment(\.colorScheme) var colorSchemevar body: some View {Text("當前模式:\(colorScheme == .dark ? "深色" : "淺色")").padding()} }
-
數據流通:
- 系統或父 View 修改環境值(如 Light ? Dark)
- 對應
@Environment
自動發事件 - 依賴該環境值的 View 失效 → 重算
body
→ 更新
-
應用場景:
- 響應系統變化:自動適配深淺色模式、動態字體大小、本地化區域等。
9.3 完整數據流動流程
- 修改狀態(
@State
、@Published
、環境值…) - Publisher 發事件(Combine)
- SwiftUI 標記失效(invalidate)
- 重新執行
body
(body engine) - Diff & 渲染(最小化 UI 更新)
9.4 選用指南與實踐
場景 | 屬性包裝器 |
---|---|
僅在當前 View 內簡單變化 | @State |
父–子組件需雙向讀寫同一狀態 | @Binding |
View 首次創建并擁有需在 View 生命周期內持有復雜對象 | @ObservableObject |
外部創建、由多個 View 訂閱的 ObservableObject | @ObservedObject |
跨多層級、全局共享的 ObservableObject | @EnvironmentObject |
讀取系統或自定義環境配置 | @Environment |
最佳實踐:
- 明確“狀態擁有者”(Owner)與“狀態訂閱者”(Subscriber)。
- 保持狀態最小化——不必要不要提升到全局,減少不必要的刷新。
- 善用屬性包裝器組合(如
@State
+@Binding
),提高組件復用性和可測試性。