SwiftUI 状态管理完全指南
从 @State / Binding,到 ObservableObject,再到 iOS 17 @Observable:选型、迁移与新手翻车点一次讲清。
1状态管理到底在管什么
声明式 UI 的核心公式:UI = f(state)。你改的是数据,SwiftUI 负责重算 body。新手最常卡在:该用哪个包装器、对象该谁创建、为啥一转屏数据没了。
@Observable。结构参考掘金《SwiftUI 状态管理完全指南》,并按新手实操改写。原文链接@Observable(最低 iOS 17),旧项目仍要会 ObservableObject。2@State:视图私有状态
最基础的工具。适合开关、输入框草稿、计数器——只给当前 View 用的简单值。
struct CounterView: View {
@State private var count = 0 // 建议 private
@State private var name = ""
var body: some View {
VStack(spacing: 12) {
Text("点了 \(count) 次")
Button("加一") { count += 1 }
TextField("名字", text: $name) // $name → Binding<String>
Text(name.isEmpty ? "还没填" : "你好,\(name)")
}
.padding()
}
}
| 要点 | 说明 |
|---|---|
| 声明 | 标 private,避免外面乱改 |
| 类型 | 优先 Int/String/Bool/小 struct;大列表别全塞这里 |
| 刷新 | 赋值变化 → SwiftUI 重算 body |
$ 前缀 | 从 @State 投影出 Binding,交给子控件读写 |
remember { mutableStateOf };≈ ArkUI V2 @Local(组件内、不可外部初始化)。真实问题:在 body 里写 var count = 0 不加 @State
View 是结构体,每次刷新都可能重建,普通属性留不住。可变 UI 状态必须进属性包装器(或外部 Store)。3@Binding:子视图改父状态
子视图不拥有数据,只拿着「通往父状态的引用」。父传 $xxx,子声明 @Binding。
struct ParentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "开" : "关")
ToggleRow(isOn: $isOn) // 注意 $
}
}
}
struct ToggleRow: View {
@Binding var isOn: Bool // 引用,不是拷贝
var body: some View {
Toggle("开关", isOn: $isOn)
}
}
$ 就变成「只传当前值的拷贝」,子改不动父。≈ Compose 的 value + onValueChange;≈ ArkUI @Param + @Event。真实问题:子视图里再声明一个 @State,和父不同步
@Binding,或把状态上提到共同父级(状态提升)。4ObservableObject + @Published(iOS 13+ 传统写法)
业务一复杂(列表、登录、网络结果),别堆几十个 @State。做成可观察的类,视图订阅它。
import Combine
final class UserViewModel: ObservableObject {
@Published var name = "张三"
@Published var age = 25
func updateName(_ newName: String) {
name = newName // @Published 会通知订阅者刷新
}
}
| 概念 | 作用 |
|---|---|
ObservableObject | 声明「我是可观察对象」(配合 Combine) |
@Published | 哪个属性变了要通知 UI |
| 方法 | 业务逻辑放类里,View 只负责展示与触发 |
@Published 的属性改了,界面通常不会自动刷(除非你手动发 objectWillChange)。5@StateObject vs @ObservedObject(必懂)
这是新手翻车第一名:对象被反复重建,列表清空、登录态丢失。
| @StateObject | @ObservedObject | |
|---|---|---|
| 谁创建 | 本视图创建并拥有 | 别人创建,我只观察 |
| 生命周期 | 视图身份保持期间只初始化一次 | 若父每次传入新实例,会跟着换 |
| 典型用法 | @StateObject private var vm = UserViewModel() | 子视图:@ObservedObject var vm: UserViewModel |
struct ContentView: View {
@StateObject private var viewModel = UserViewModel() // 拥有
var body: some View {
ChildView(viewModel: viewModel) // 往下传同一实例
}
}
struct ChildView: View {
@ObservedObject var viewModel: UserViewModel // 只观察
var body: some View {
Text(viewModel.name)
Button("改名") { viewModel.updateName("李四") }
}
}
@ObservedObject var vm = UserViewModel() 几乎一定是错的。真实问题:一旋转屏幕 / 导航回来,ViewModel 数据没了
@ObservedObject 在本页「就地 new」。改成 @StateObject,或把对象放到更上层 App / 父页面持有。6@EnvironmentObject:跨多层共享(传统)
登录态、主题配置等「整棵树都可能用到」的对象,不必一层层参数往下传。
final class AppState: ObservableObject {
@Published var isLoggedIn = false
}
@main
struct MyApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState) // 注入
}
}
}
struct ProfileView: View {
@EnvironmentObject var appState: AppState // 任意深层读取
var body: some View {
Text(appState.isLoggedIn ? "已登录" : "未登录")
}
}
.environmentObject 会在运行时直接崩溃(找不到对象)。预览里也要同样注入,否则 Preview 炸。真实问题:Preview crashed:No ObservableObject of type … found
ProfileView().environmentObject(AppState())。7@Environment:系统环境值
读系统/框架提供的值(深浅色、尺寸类、dismiss 等),不是你自己的业务 Store。
struct ThemeAwareView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack {
Text(colorScheme == .dark ? "深色模式" : "浅色模式")
Button("关闭") { dismiss() }
}
}
}
8@AppStorage / @SceneStorage(轻量持久化)
简单开关、用户名这类小数据可直接绑 UserDefaults;场景级临时状态用 SceneStorage。大列表请看第 09 章文件存储。
struct SettingsView: View {
@AppStorage("username") private var username = ""
@AppStorage("isDarkMode") private var isDarkMode = false
@SceneStorage("draft") private var draft = "" // 随场景,偏临时
var body: some View {
Form {
TextField("用户名", text: $username)
Toggle("深色模式", isOn: $isDarkMode)
TextField("草稿", text: $draft)
}
}
}
| @AppStorage | @SceneStorage | |
|---|---|---|
| 底层 | UserDefaults | 场景存储 |
| 存活 | 杀 App 还在 | 场景结束可清 |
| 适合 | 设置项、开关 | 滚动位置、未提交草稿 |
9@Observable(iOS 17+ 推荐)
现代写法:一个宏搞定可观察类,不用 ObservableObject / @Published。新项目优先学这个。
import SwiftUI
@Observable
final class UserModel {
var name = "张三"
var age = 25
var email = "zhangsan@example.com"
}
struct ContentView: View {
@State private var user = UserModel() // 用 @State 持有(拥有)
var body: some View {
VStack {
Text("姓名:\(user.name)")
TextField("修改姓名", text: $user.name)
}
}
}
更简洁
去掉协议与一堆 @Published。
更精细
按访问的属性刷新,减少无谓重绘。
持有方式
本页拥有 → @State;需要子页双向绑属性 → @Bindable。
10@Bindable:给 @Observable 做双向绑定
父把 @Observable 实例传给子;子要用 $settings.xxx 时,给参数加 @Bindable。
@Observable
final class Settings {
var isDarkMode = false
}
struct ParentView: View {
@State private var settings = Settings()
var body: some View {
ChildView(settings: settings)
}
}
struct ChildView: View {
@Bindable var settings: Settings
var body: some View {
Toggle("深色模式", isOn: $settings.isDarkMode)
}
}
11@Environment + @Observable(现代全局态)
@Observable
final class AppState {
var isLoggedIn = false
var userName = ""
}
@main
struct MyApp: App {
@State private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environment(appState) // 注意:不是 environmentObject
}
}
}
struct ProfileView: View {
@Environment(AppState.self) private var appState
var body: some View {
Text(appState.isLoggedIn ? "欢迎 \(appState.userName)" : "未登录")
}
}
environmentObject + @EnvironmentObject。现代:environment(_:) + @Environment(Type.self)。别混用同一套注入/读取 API。12怎么选:一张表
| 场景 | iOS 13–16 | iOS 17+(推荐) |
|---|---|---|
| 单个视图内部 | @State | @State |
| 父子共享简单值 | @Binding | @Binding |
| 本页业务模型 | @StateObject + ObservableObject | @State + @Observable |
| 子页观察/绑定模型 | @ObservedObject | 传入 + 必要时 @Bindable |
| 全局共享 | @EnvironmentObject | @Environment(Type.self) |
| 小开关持久化 | @AppStorage | @AppStorage |
@Observable,少踩 StateObject/ObservedObject 的坑。13从 ObservableObject 迁到 @Observable
final class OldVM: ObservableObject {
@Published var text = ""
}
struct OldView: View {
@StateObject private var vm = OldVM()
var body: some View {
TextField("", text: $vm.text)
}
}
@Observable
final class NewVM {
var text = ""
}
struct NewView: View {
@State private var vm = NewVM()
var body: some View {
TextField("", text: $vm.text)
}
}
14迷你实战:待办 Store(现代写法)
把「状态 + 行为」收进一个模型;界面只负责渲染与调用。完整持久化版见第 12 章。
@Observable
final class TodoViewModel {
struct Todo: Identifiable, Equatable {
let id = UUID()
var title: String
var isCompleted = false
}
var todos: [Todo] = []
var newTitle = ""
func addTodo() {
let t = newTitle.trimmingCharacters(in: .whitespacesAndNewlines)
guard !t.isEmpty else { return }
todos.insert(Todo(title: t), at: 0)
newTitle = ""
}
func toggle(_ todo: Todo) {
guard let i = todos.firstIndex(of: todo) else { return }
todos[i].isCompleted.toggle()
}
}
struct TodoListView: View {
@State private var vm = TodoViewModel()
var body: some View {
NavigationStack {
List {
ForEach(vm.todos) { todo in
HStack {
Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
.onTapGesture { vm.toggle(todo) }
Text(todo.title)
.strikethrough(todo.isCompleted)
}
}
}
.navigationTitle("待办")
.toolbar {
ToolbarItem(placement: .bottomBar) {
HStack {
TextField("新待办", text: $vm.newTitle)
Button("添加") { vm.addTodo() }
}
}
}
}
}
}
15你会鸿蒙时:快速对照
| SwiftUI | ≈ ArkUI V2 / MVVM |
|---|---|
@State | @Local |
@Binding | @Param + @Event |
@Observable 类 | @ObservedV2 + @Trace ViewModel |
@Environment / environment | @Provider / @Consumer、AppStorageV2 |
@AppStorage | Preferences / AppStorageV2 小键值 |
16踩坑 FAQ
改了数组里某个元素的字段,界面不刷新?
@Published 数组,改 todos[i].done 有时观测不到。写成替换元素或整表赋值;或用 @Observable 并确保触发了属性写。模型用 struct + 替换下标更稳。子视图预览单独打开就崩
所有状态都塞进全局 Environment 好不好?
@State 能存 class 吗?
@State 持有 @Observable 类是官方路径。更早系统对 class 请用 @StateObject。17本章收口
基础层
@State、@Binding
传统对象层
ObservableObject、StateObject/ObservedObject、EnvironmentObject
持久化小品
@AppStorage、@SceneStorage
现代层
@Observable、@Bindable、@Environment(Type.self)