第四章:状态管理基础

状态管理,说白了就是让数据和界面“同步”。

我刚学 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 框架,看看怎么处理更复杂的异步数据流。