第四章:状态管理基础
状态管理,说白了就是让数据和界面“同步”。
我刚学 SwiftUI 时,最困惑的就是:为什么我改了变量,界面没反应?后来才明白,SwiftUI 有一套自己的“监控系统”。你得告诉它:“嘿,这个数据变了,刷新一下!”
这一章,我们就来拆解这套系统。从最简单的 @State,到复杂的 @StateObject,一步步讲清楚。
4.1 @State 属性包装器:视图的本地状态
@State 是 SwiftUI 里最基础的状态管理工具。它专门用来管理“当前视图自己”的数据。
核心要点:@State 修饰的变量一旦改变,SwiftUI 会自动重新计算 body,刷新界面。
举个例子。你做一个计数器,点击按钮数字加一。用 @State 就很简单:
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
Text("当前计数:\(count)")
.font(.largeTitle)
Button("点击加一") {
count += 1
}
}
}
}
注意看,count 前面有个 @State。这意味着:
- count 变化时,Text 会自动更新
- count 是私有的,只能在这个视图里用
- SwiftUI 会帮我们管理它的生命周期
我的习惯:@State 只用来存“简单值”,比如 Bool、Int、String。复杂对象别用 @State,容易出问题。
我曾经犯过一个错:把一个模型对象用 @State 存,结果改了属性界面不刷新。为什么?因为 @State 只监控变量本身,不监控变量的内部属性。嗯,这个坑后面会细说。
4.2 @Binding 双向绑定:父子视图传数据
@Binding 解决的是“子视图怎么修改父视图的数据”这个问题。
你想想看,如果子视图想改父视图的 @State,直接传过去是不行的。因为 @State 是私有的。这时候就需要 @Binding——它像一根“数据线”,把父视图的数据“引用”到子视图。
struct ParentView: View {
@State private var isOn = false
var body: some View {
ToggleView(isOn: $isOn)
}
}
struct ToggleView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("开关", isOn: $isOn)
}
}
注意 $isOn 这个写法。$ 符号表示“取绑定”,把 @State 变成 @Binding 传过去。子视图里用 @Binding 接收,修改 isOn 时,父视图的 isOn 也会跟着变。
避坑指南:我曾经在子视图里直接修改 @Binding 的值,但忘了传 $ 符号。结果编译通过了,界面死活不更新。检查了半天才发现,传的是普通值,不是绑定。记住:父传子用 $,子接收用 @Binding。
4.3 @ObservedObject 与 @Published:跨视图共享数据
当你的数据需要被多个视图共享时,@State 就不够用了。这时候需要 @ObservedObject。
@ObservedObject 配合 @Published 使用,可以监控一个“可观察对象”的属性变化。
class UserSettings: ObservableObject {
@Published var username = "默认用户"
@Published var isLoggedIn = false
}
struct ProfileView: View {
@ObservedObject var settings: UserSettings
var body: some View {
VStack {
Text("用户名:\(settings.username)")
TextField("输入用户名", text: $settings.username)
}
}
}
这里的关键点:
- UserSettings 继承 ObservableObject
- 需要监控的属性加 @Published
- 使用这个对象的视图用 @ObservedObject
个人经验:我在项目中用 @ObservedObject 管理用户配置、网络请求结果等。但要注意,@ObservedObject 不负责对象的生命周期。如果对象被释放了,视图会崩溃。
为什么会这样?因为 @ObservedObject 只是“观察”对象,它不“持有”对象。对象由创建它的地方管理。如果创建者释放了对象,观察者还在用,就出事了。
4.4 @StateObject 生命周期:谁创建,谁负责
@StateObject 是 SwiftUI 2.0 引入的。它和 @ObservedObject 很像,但多了一个能力:管理对象的生命周期。
说白了,@StateObject 会“持有”这个对象。对象随着视图创建而创建,随着视图销毁而销毁。不会出现“对象被提前释放”的问题。
struct ContentView: View {
@StateObject var settings = UserSettings()
var body: some View {
ProfileView(settings: settings)
}
}
这里 settings 由 ContentView 创建并持有。ProfileView 用 @ObservedObject 接收,只观察不持有。这样分工明确:
- 创建者用 @StateObject
- 接收者用 @ObservedObject
我的建议:如果你不确定用哪个,记住这个原则——谁 new 这个对象,谁就用 @StateObject。其他视图一律用 @ObservedObject。
我曾经在项目里把 @StateObject 用在了子视图上,结果每次父视图刷新,子视图的 @StateObject 就重新创建一次。数据全丢了。嗯,这个教训挺深刻的。
4.5 四种状态管理的对比
为了方便记忆,我整理了一个表格:
| 属性包装器 | 用途 | 生命周期 | 典型场景 |
|---|---|---|---|
| @State | 视图本地状态 | 随视图创建/销毁 | 计数器、开关、输入框 |
| @Binding | 父子视图数据同步 | 由源数据决定 | 自定义控件、表单组件 |
| @ObservedObject | 观察外部对象 | 不持有,由外部管理 | 共享数据、网络请求结果 |
| @StateObject | 创建并持有对象 | 随视图创建/销毁 | 数据源、ViewModel |
重要提醒:@StateObject 只在 iOS 14+ 可用。如果你的项目还支持 iOS 13,只能用 @ObservedObject 配合 @State 手动管理生命周期。
4.6 实战:一个完整的登录界面
最后,我们把这些知识点串起来,做一个简单的登录界面。
class LoginViewModel: ObservableObject {
@Published var username = ""
@Published var password = ""
@Published var isLoggingIn = false
@Published var errorMessage: String?
func login() {
guard !username.isEmpty, !password.isEmpty else {
errorMessage = "用户名和密码不能为空"
return
}
isLoggingIn = true
// 模拟网络请求
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.isLoggingIn = false
self.errorMessage = nil
}
}
}
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
var body: some View {
VStack(spacing: 20) {
TextField("用户名", text: $viewModel.username)
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("密码", text: $viewModel.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
if let error = viewModel.errorMessage {
Text(error)
.foregroundColor(.red)
}
Button("登录") {
viewModel.login()
}
.disabled(viewModel.isLoggingIn)
}
.padding()
}
}
这个例子展示了:
- @StateObject 创建并持有 ViewModel
- @Published 监控用户名、密码、登录状态
- $ 绑定让 TextField 和 ViewModel 双向同步
- 错误信息通过 @Published 自动显示
嗯,到这里,状态管理的基础就讲完了。下一章我们会深入 Combine 框架,看看怎么处理更复杂的异步数据流。