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;介绍如何…