SwiftUI學習筆記day4: Lecture 4 | Stanford CS193p 2023

Lecture 4 | Stanford CS193p 2023

  • 課程鏈接:https://www.youtube.com/watch?v=4CkEVfdqjLw

  • 代碼倉庫:iOS

  • 課程大綱:

    簡要課程大綱:SwiftUI 高級主題

    1. Swift 訪問控制(Access Control)
      • 5 個級別:openpublicinternalfileprivateprivate
      • private(set)fileprivate(set) 的用法
      • 在 SwiftUI 視圖與模塊化中的最佳實踐
    2. 視圖初始化(init)與屬性包裝器配合
      • 合成 init 與自定義 init 時機
      • init 中正確配置:
        • @Binding(父–子雙向綁定)
        • @ObservedObject(外部傳入模型)
        • @StateObject(首次創建模型)
      • init 中的限制與應將副作用延后到 onAppear
    3. 循環與遍歷 (for-in)
      • 遍歷數組、字典、范圍 (Range)
      • enumerated() 獲取索引
      • where 條件過濾、break/continue
      • 修改原集合的技巧
    4. 函數類型與閉包(Functions & Closures)
      • 函數即類型:() -> Void(Int) -> String() -> some View
      • 回調參數與自定義 ViewBuilder
      • 閉包語法簡寫:類型推斷、$0、省略 return
      • 捕獲(Capturing):閉包如何“包住”外部變量
    5. 異步與逃逸閉包 (@escapingasync/await)
      • 何時使用 @escaping:網絡請求、GCD、定時器
      • SwiftUI 中的異步:Task { await … }.task & 按鈕內部
      • 結合 @MainActor 回到主線程更新狀態
    6. 類型級成員:static 變量與函數
      • 與實例無關的常量、工具方法
      • 共享樣式、格式化器、預覽提供者 (PreviewProvider)
      • struct/enum 命名空間模式
    7. 值類型方法的 mutating
      • 為什么值類型默認不可變
      • 在模型層封裝可變邏輯:mutating func
      • SwiftUI 中通過 @State/@Binding 替代直接使用
    8. 語義化重命名(Semantic Rename)
      • Xcode Refactor → Rename 操作
      • 跨文件、跨模塊安全重命名接口/類型
      • 保持項目代碼一致性
    9. SwiftUI 響應式 UI 與狀態管理
      • 單一真相聲明式+響應式 流程
        1. 狀態改變(@State@Published、環境值)
        2. Combine Publisher 發事件
        3. SwiftUI 標記 View 失效 → 重算 body → diff → 渲染
      • 屬性包裝器詳解:
        • @State:局部輕量狀態
        • @Binding:父–子雙向綁定
        • @StateObject:首次創建并擁有的模型
        • @ObservedObject:外部傳入并訂閱的模型
        • @EnvironmentObject:跨層級共享模型
        • @Environment: 系統/自定義環境值
      • 典型場景示例與對比

文章目錄

  • 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)承擔著以下核心職責:

  1. 接收外部參數并初始化存儲屬性
  2. 配置屬性包裝器(@State、@Binding、@ObservedObject、@StateObject 等)
  3. 決定視圖的初始狀態

下面從幾個角度詳細說明 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 中的限制與行為

  1. body 尚不可用
    init 執行時 body 尚未構建,不要在 init 里觸發視圖渲染或依賴 body 屬性。
  2. 禁止在 init 中做副作用
    避免在 init 中執行網絡請求、定時器啟動等副作用;應把這類邏輯放在 onAppear 或視圖模型里。
  3. @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 使用 breakcontinue

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 } 無需 returnSwift 自動返回表達式結果

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]

  • countmakeCounter 函數里的局部變量。
  • 閉包 { 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 中常見的閉包捕獲用法

  1. 計數器(如上例)
  2. 異步回調(需要捕獲某些狀態)
  3. SwiftUI 的動畫或響應事件回調
  4. 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 開始,你可以用 asyncawait 寫更清晰的異步邏輯,配合 SwiftUI 的 .taskButton

🔧 示例:

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)}
    }
    
  • 特點

    1. 與實例無關:不需要創建 ContentView() 實例,就可以直接通過 ContentView.defaultTitle 訪問。
    2. 共享:在所有實例中只有一份存儲或計算邏輯,可用于緩存重用,比如 NumberFormatterDateFormatter、自定義樣式等。
    3. 延遲初始化:存儲型 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 只能用于值類型(如 structenum)或 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 使用建議

  1. 常量、共享資源:將不隨實例變化的配置、樣式、Formatter 等放進 static let
  2. 輔助函數:與視圖實例狀態無關的純函數可聲明為 static func
  3. 命名空間:可利用 struct + static 對一組相關常量/方法進行邏輯分組。

7.semantic rename

在Xcode中我們如果想對一個接口或者類型進行修改命名的話,如果我們直接手動修改會比較麻煩,并且會導致修改錯誤;

這時候我們就可以借Xcode提供的修改接口來完成我們的修改操作:

  • Step1:選擇你要修改類型名右鍵之后選擇refactor->rename

在這里插入圖片描述

  • Step2: 在輸入框里寫入修改后的名稱,然后點擊rename 就好了!

在這里插入圖片描述

8.mutating

在 Swift 里,mutating 是一個修飾符,用在值類型structenum)的方法前,表示這個方法會修改它自身(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

  1. 自定義業務模型(純 Swift 結構體)
    • 將可變邏輯封裝在模型內部,用 mutating 標記。
    • 通過 @State@Binding@ObservedObject 在 View 中引用模型實例。
  2. 擴展值類型
    • 例如給自定義 ShapeLayoutPreferenceKey 等結構體添加修改自身狀態的方法。
  3. 協議實現里需要修改自身
    • 某些協議(如自定義的協議)要求方法能改變結構體屬性,就要在方法簽名前加 mutating

8.4 小結

  • Swift 語言層面mutating 讓值類型方法能夠改變 self
  • SwiftUI 層面:大多數狀態變化都是通過屬性包裝器來實現,你很少在 View 上直接寫 mutating
  • 最佳實踐:如果模型本身是值類型,而且你想把修改邏輯封裝進去,別忘了在方法前加 mutating;在 View 里就直接調用模型方法或操作 @State@Binding 即可。

9.Swift中的狀態管理

9.1 核心理念

  • 聲明式+響應式:UI 聲明“我想展示什么”,狀態改變后自動“重繪”界面。
  • 單一真相(Single Source of Truth):狀態(State)是唯一可靠的數據源,所有 UI 都從它派生。
  • 響應式更新流程
    1. 狀態改變(@State、@Published…)
    2. Publisher 發事件
    3. SwiftUI 標記失效
    4. 重新執行 body
    5. 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()}
    }
    
  • 數據流通

    1. count += 1 → 寫入內部“狀態槽”
    2. @State 底層是 Combine Publisher,發出新值事件
    3. SwiftUI 標記該 View 失效 → 下一幀調用 body
    4. 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()}
    }
    
  • 數據流通

    1. 子 View 調用 isOn.toggle()
    2. 實際修改父 View 的 @State isOn → 觸發 @State 流程
    3. 父 View 重算 body,通過 $isOn 傳回最新值給子 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)}
    }
    
  • 數據流通

    1. model.seconds += 1@Published 發事件
    2. SwiftUI 捕獲事件 → 標記 TimerView 失效
    3. 重算 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) }
    }
    
  • 數據流通

    1. 外部某處 settings.username = "新名"@Published 發事件
    2. 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)") }
    }
    
  • 數據流通

    1. 任意層級調用 userData.name = "新名"@Published 發事件
    2. 所有訂閱該對象的 View 都失效 → 各自重算 body → 更新
  • 應用場景

    • 跨模塊共享:用戶設置、全局主題、購物車數據等需要在 App 多處訪問的全局狀態。

6. @Environment

  • 定義:讀取系統或自定義的環境值(如配色方案、字體、布局方向等)。

  • 示例

    struct ThemedView: View {@Environment(\.colorScheme) var colorSchemevar body: some View {Text("當前模式:\(colorScheme == .dark ? "深色" : "淺色")").padding()}
    }
    
  • 數據流通

    1. 系統或父 View 修改環境值(如 Light ? Dark)
    2. 對應 @Environment 自動發事件
    3. 依賴該環境值的 View 失效 → 重算 body → 更新
  • 應用場景

    • 響應系統變化:自動適配深淺色模式、動態字體大小、本地化區域等。

9.3 完整數據流動流程

  1. 修改狀態@State@Published、環境值…)
  2. Publisher 發事件(Combine)
  3. SwiftUI 標記失效(invalidate)
  4. 重新執行 body(body engine)
  5. Diff & 渲染(最小化 UI 更新)

9.4 選用指南與實踐

場景屬性包裝器
僅在當前 View 內簡單變化@State
父–子組件需雙向讀寫同一狀態@Binding
View 首次創建并擁有需在 View 生命周期內持有復雜對象@ObservableObject
外部創建、由多個 View 訂閱的 ObservableObject@ObservedObject
跨多層級、全局共享的 ObservableObject@EnvironmentObject
讀取系統或自定義環境配置@Environment

最佳實踐

  1. 明確“狀態擁有者”(Owner)與“狀態訂閱者”(Subscriber)。
  2. 保持狀態最小化——不必要不要提升到全局,減少不必要的刷新。
  3. 善用屬性包裝器組合(如 @State + @Binding),提高組件復用性和可測試性。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/910602.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/910602.shtml
英文地址,請注明出處:http://en.pswp.cn/news/910602.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Docker 高級管理——容器通信技術與數據持久化

目錄 一、Docker 容器的網絡模式 1. Bridge 模式 2. Host 模式 3. Container 模式 4. None 模式 5. Overlay 模式 6. Macvlan 模式 7. 自定義網絡模式 二、端口映射 1. 端口映射 2. 隨機映射端口 3. 指定映射端口 &#xff08;1&#xff09;固定端口 &#xff08;…

git操作案例 -設置遠程分支,并提交到新遠程新分支

文章目錄 前言一、分析當前的問題二、修改遠程倉庫地址&#xff08;一&#xff09;修改遠程倉庫地址場景 現有保留遠程分支場景替換現有遠程分支 二、 找回已經提交的文件場景&#xff1a;提交后&#xff0c;代碼在本地倉庫但未推送 三、同步遠程分支四、提交到新遠程的新分支 …

mysql一張表,其中一個字段設置了唯一索引,又設置了普通索引,查詢的時候很慢,沒有走普通索引,是const

問題分析 在 MySQL 中&#xff0c;當一個字段同時存在唯一索引和普通索引時&#xff0c;查詢優化器通常會優先選擇最嚴格的索引&#xff08;即能最快縮小結果集的索引&#xff09;。在你的場景中&#xff0c;優化器選擇了唯一索引并將查詢視為const類型&#xff0c;這通常是高…

ARCGIS國土超級工具集1.6更新說明

ARCGIS國土超級工具集V1.6版本&#xff0c;功能已增加至60 個。本次更新在V1.5版本的基礎上&#xff0c;除修復了使用時發現的若干小問題外&#xff0c;還更新及新增了若干工具。其中勘測定界工具欄更新了界址點西北角重排工具&#xff0c;新增了提示圖斑起始點、指定圖斑起始點…

零基礎學習RabbitMQ(2)--Linux安裝RabbitMQ

注意&#xff1a;這里使用的是ubuntu系統 1. 安裝Erlang RabbitMQ需要Erlang語言的支持&#xff0c;在安裝rabbitMQ之前需要安裝Erlang #更新軟件包 sudo apt-get update #安裝erlang sudo apt-get install erlang 安裝后輸入 rel可查看Erlang版本&#xff1a; 輸入halt().…

Centos進單用戶模式

一、開機按E 二、修改里面的linux行 把ro 修改成rw init/sysroot/bin/sh 修改前&#xff1a; 修改后&#xff1a; 三、ctrl x退出&#xff0c;進入單用戶模式

RabbitMQ 的工作流程

RabbitMQ 是一個消息中間件&#xff0c;實現了生產者消費者模型&#xff0c;可以用來接收、存儲、轉發消息。 專有名詞介紹 要了解 RabbitMQ 的工作流程&#xff0c;我們需要先了解下面幾個關鍵詞&#xff1a; 1、Producer 生產者&#xff0c;即向 RabbitMQ 發送消息。 2…

HTTP——不同版本區別

目錄 HTTP1.0和HTTP1.1的區別 HTTP1.1相比HTTP1.0性能上的改進&#xff1a; 但是HTTP1.1還是有性能瓶頸&#xff1a; HTTP/2做了什么優化&#xff1f; HTTP/3的優點 HTTP與HTTPS的區別 HTTPS的工作原理 1.ClientHello 2.ServerHello 3.客戶端回應 4.服務器的最后回應…

關于M0+芯片的IAP應用導致延時不準確解釋

前言&#xff1a;在給項目中使用的M0芯片做IAP功能時一切一切都是那么的自然水到渠成&#xff0c;但是筆者在實現完IAP功能后&#xff0c;卻發現APP端掛載的單總線功能崩潰了&#xff0c;最開始沒有懷疑是bootload導致的。因為筆者在使用同一篇代碼的時候單總線掛載的設備不同&…

安卓登錄學習筆記

1. 背景與目標 (Background and Goal) 背景: 我們要創建一個用戶登錄界面。用戶輸入用戶名和密碼&#xff0c;點擊“登錄”按鈕。應用會顯示一個加載中的“圈圈”&#xff08;ProgressBar&#xff09;&#xff0c;然后模擬一個耗時2秒的網絡請求。根據請求結果&#xff0c;界面…

Git(三):分支管理

文章目錄 Git(三)&#xff1a;分支管理理解分支創建分支切換分支合并分支刪除分支合并沖突分支管理策略分支策略Bug分支刪除臨時分支 Git(三)&#xff1a;分支管理 理解分支 本章介紹Git的殺手級功能之一&#xff1a;分支 分支就 是科幻電影里面的平行宇宙&#xff0c;當你正…

電子電氣架構 --- 電氣架構基礎(汽車電子)

我是穿拖鞋的漢子,魔都中堅持長期主義的汽車電子工程師。 老規矩,分享一段喜歡的文字,避免自己成為高知識低文化的工程師: 做到欲望極簡,了解自己的真實欲望,不受外在潮流的影響,不盲從,不跟風。把自己的精力全部用在自己。一是去掉多余,凡事找規律,基礎是誠信;二是…

RestClient 功能介紹、完整使用示例演示, 和RestTemplate、WebClient 對比

RestClient功能介紹 RestClient是Spring Framework 6.1版本引入的同步HTTP客戶端&#xff0c;旨在替代老舊的RestTemplate&#xff0c;提供更現代、流暢的API設計。其核心特點包括&#xff1a; 流暢API&#xff08;Fluent API&#xff09;&#xff1a; 支持鏈式調用&#xff0…

VM經常遇見的運行慢幾種情況、以及設置方法

大家好,我是東哥說-MES 啟動虛擬機是提示如下內容 “無法打開內核設備“\.\VMCIDev\VMX”: 操作成功完成。是否在安裝 VMware Workstation 后重新引導? 模塊“DevicePowerOn”啟動失敗。 未能啟動虛擬機。” 2.用記事本打開安裝目錄下TIA Portal STEP7 Prof Safety WinCC …

【C++語法】類和對象(4)——日期類和const成員函數

6.類和對象&#xff08;4&#xff09; 文章目錄 6.類和對象&#xff08;4&#xff09;回顧簡單日期類的實現代碼補充&#xff1a;前置與后置的重載區別補充&#xff1a;關于流插入運算符&#xff08;<<&#xff09;的解釋拓展&#xff1a;仿照流插入操作符(<<)的作…

當凌晨的鍵盤聲,遇見黎明的星光?

地鐵玻璃映出你困倦的臉&#xff0c;耳機里的音樂循環到第 17 遍&#xff0c;早高峰的人群像沙丁魚罐頭般擠壓著你。這是你每天雷打不動的三小時通勤路&#xff0c;從城市邊緣到寫字樓林立的 CBD&#xff0c;窗外的風景換了四季&#xff0c;而你始終困在搖晃的車廂里&#xff0…

Web Worker技術詳解與應用場景

我們來詳細探討一下 Web Worker。它是現代 Web 開發中解決 JavaScript 單線程限制、提升應用性能和響應能力的關鍵技術。 核心問題&#xff1a;JavaScript 的單線程模型 瀏覽器 UI 線程&#xff08;主線程&#xff09;&#xff1a;JavaScript 在瀏覽器中默認運行在單個線程&a…

React Next快速搭建前后端全棧項目并部署至Vercel

很好&#xff0c;你是想搞清楚Next.js 的后端結構和傳統 Node Express 的區別對比&#xff0c;我來整理一套結構化、精準、對面試有說服力的解答&#xff0c;并附示意結構圖。 01Next vs Express 、## ??1?? Next.js 后端是怎么構建的 Next.js 在默認情況下本身就集成后…

【T寶客戶項目解決過程】01-模型訓練

1 項目需求描述 博主自己開了一家T寶店&#xff0c;有一個客戶有這個需求&#xff1a;有一大堆圖像&#xff0c;大概有10多萬張圖&#xff0c;都是比較小尺寸的圖。各種類型都有&#xff0c;我們想要通過將不同類型發圖像進行分開&#xff0c;如何實現呢&#xff1f; 2 思路 …

如何在中將網絡改為橋接模式并配置固定IP地址

在使用服務器搭建虛擬機的過程中&#xff0c;我們發現有許多場景需要將虛擬機的網絡配置為橋接模式&#xff0c;并為其設置固定的IP地址。為了幫助大家更高效地進行網絡配置&#xff0c;提升虛擬機的連接穩定性和管理便捷性&#xff0c;我們總結了這篇指南&#xff0c;介紹如何…