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
),提高组件复用性和可测试性。