
引言
在 SwiftUI 中,界面并不是通过手动刷新来更新的,而是由状态驱动的 。当状态发生变化,SwiftUI 会自动识别哪些视图需要重绘,从而保持 UI 与数据的一致性。这种声明式的方式大大简化了界面开发的流程,但也带来一个问题:状态到底该怎么管理,才能让视图"正确地"更新?
SwiftUI 提供了多种状态绑定机制,包括 @State、@ObservedObject 和 @EnvironmentObject。它们虽然都是用来驱动视图更新,但适用的场景、生命周期、绑定方式却各不相同。一不小心,可能就会遇到"明明数据变了,界面却不更新"的尴尬场面。
这篇文章将深入讲解 SwiftUI 中的三种主要数据绑定方式,结合具体的使用场景和代码实例,帮助你理清它们的使用逻辑,掌握最佳实践,避免常见误区。无论你是刚接触 SwiftUI 的新手,还是已经在项目中使用它的开发者,这篇文章都能为你在构建可维护、响应式的界面上提供帮助。
实战场景:用一个用户页面串起三种状态绑定方式
为了更直观地理解 SwiftUI 中三种核心状态绑定方式的使用场景和区别,我们来构建一个实际项目中常见的页面 ------ MineView,即"个人中心"页面。
这个页面的功能需求如下:
- 展示用户信息:包括昵称与金币数量。
- 金币显示开关:点击"小眼睛"图标可以切换金币的隐藏与显示。
- 支持页面跳转:例如跳转到设置页或其他模块。
针对这些需求,我们分别会用到:
- @State:用于控制金币是否显示,这是一个纯粹的视图内部状态;
- @ObservedObject:用于监听用户数据模型 PHUserHelper 中的金币和昵称变化,这是一个绑定外部可观察对象的状态;
- @EnvironmentObject:用于全局路由控制,通过 RouterHelper 管理跳转,是一个跨页面共享的全局状态。
接下来,我们将按功能拆解的顺序,依次介绍这三种状态绑定方式的使用方法与最佳实践。
1. 管理局部状态:@State 控制金币隐藏/显示
在 SwiftUI 中,@State 是最轻量也是最常用的状态绑定方式。它适用于视图自身内部的小范围状态管理,比如按钮选中、输入框内容、视图显隐等场景。
在我们的 MineView 页面中,用户可以点击一个"眼睛"图标,切换金币是否可见。这种行为是一个纯粹的 UI 控制,不涉及外部数据源,因此非常适合使用 @State 来管理。
Swift
import Foundation
import SwiftUI
struct MineView: View {
/// 控制金币是否显示
@State private var showGold = true
var body: some View {
HStack(spacing: 12) {
Text("金币:")
.font(.headline)
// 根据状态展示金币数量或密文
Text(showGold ? "1280" : "****")
.bold()
// 小眼睛按钮,用于切换状态
Button(action: {
showGold.toggle()
}) {
Image(systemName: showGold ? "eye" : "eye.slash")
.foregroundColor(.blue)
}
}
.padding()
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
}) {
Image(systemName: "chevron.left")
.foregroundColor(.black)
}
}
ToolbarItem(placement: .principal) {
Text(LanguageHelper.localizedString(for: "my_title"))
.font(.headline)
.foregroundColor(.primary)
}
}
}
}
- @State 修饰的变量 showGold 是一个 局部状态,只在当前视图中使用;
- 当 showGold 的值发生变化时,SwiftUI 会自动刷新依赖它的 UI(即 Text 和 Image);
- SwiftUI 中的视图是值类型,@State 让这些值类型视图也拥有"持久状态"的能力。


场景 | 是否适合用@State |
---|---|
控制某个按钮是否选中 | ✅ 是 |
输入框的实时文本绑定 | ✅ 是 |
控制一个弹窗是否弹出 | ✅ 是 |
管理整个用户对象或大型数据结构 | ❌ 否,考虑 @ObservedObject |
2. 监听数据变化:@ObservedObject 实时更新用户信息
当视图需要响应某个外部对象的属性变化,比如用户昵称或金币数量,就需要使用 @ObservedObject。
在我们的场景中,用户信息由一个单例类 PHUserHelper 管理,并持有一个 PHUser 模型。我们希望当用户的金币数量或昵称更新时,MineView 页面能自动刷新显示的数据。此时就可以用 @ObservedObject 来监听这些变化。
模型设计
首先,我们定义一个 PHUser 用户模型,并通过 @Published 修饰其属性,确保它们发生变化时会通知观察者(比如视图)。
Swift
class PHUser: ObservableObject {
@Published var nickname: String = "未登录"
@Published var gold: Int = 0
}
然后我们创建一个用户管理类 PHUserHelper,作为单例提供全局访问。
Swift
class PHUserHelper: ObservableObject {
static let shared = PHUserHelper()
@Published var user = PHUser()
}
视图中的使用
Swift
struct MineView: View {
/// 控制金币是否显示
@State private var showGold = true
/// 监听用户管理器
@ObservedObject var helper = PHUserHelper.shared
var body: some View {
VStack(alignment: .center, spacing: 12) {
// 显示用户昵称
Text("欢迎你,\(helper.user.nickname)")
.font(.title2)
HStack(spacing: 12) {
Text("金币:")
.font(.headline)
// 根据状态展示金币数量或密文
Text(showGold ? "\(helper.user.gold)" : "****")
.bold()
// 小眼睛按钮,用于切换状态
Button(action: {
showGold.toggle()
}) {
Image(systemName: showGold ? "eye" : "eye.slash")
.foregroundColor(.blue)
}
}
}
.padding()
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
}) {
Image(systemName: "chevron.left")
.foregroundColor(.black)
}
}
ToolbarItem(placement: .principal) {
Text(LanguageHelper.localizedString(for: "my_title"))
.font(.headline)
.foregroundColor(.primary)
}
}
}
}
- @ObservedObject 修饰的对象必须是遵循了 ObservableObject 协议的类。
- 被观察对象的属性必须使用 @Published 标记,否则属性改变不会触发视图更新。
- 在视图中使用对象属性(如 helper.user.gold)时,SwiftUI 会建立"依赖关系",从而在属性变动时自动刷新对应 UI。
3. 跨页面共享状态:@EnvironmentObject 实现路由跳转与全局通信
在 SwiftUI 中,@EnvironmentObject 是一种在多个视图层级间共享数据的方式,适用于跨页面的全局状态管理,比如:用户信息、App 设置、导航跳转、主题控制等。
在我们的场景中,MineView 可以跳转到 EditView,用户在编辑页中修改昵称后返回,主页面应能自动刷新。为了不手动传递路由器对象或用户对象,我们使用 @EnvironmentObject 注入共享实例。
路由管理器:RouterHelper
需要继承自ObservableObject,代码如下:
Swift
class RouterHelper: ObservableObject {
static let shared = RouterHelper()
/// 路径数组,代表导航栈
@Published var path: [PDFRoute] = []
private init() {}
/// 跳转到某个路由
func push(_ route: PDFRoute) {
path.append(route)
}
/// 返回上一级页面
func pop() {
if !path.isEmpty {
path.removeLast()
}
}
/// 返回到指定页
/// - Parameter index: 要返回到的页面索引
func popTo(index: Int) {
guard index >= 0 && index < path.count else { return }
path = Array(path.prefix(upTo: index + 1))
}
/// 返回首页,清空路径
func popToRoot() {
path.removeAll()
}
}
路由注入及使用
我们通过 .environmentObject() 将路由管理器注入到mine页及编辑页。
Swift
case .mine:
MineView()
.environmentObject(router)
Swift
case .edit:
// 编辑页面
EditView()
.environmentObject(RouterHelper.shared)
在 MineView 中使用 @EnvironmentObject 接收这个路由对象,并触发跳转:
Swift
struct MineView: View {
@EnvironmentObject var router: RouterHelper
@ObservedObject var user: PHUser
@State private var showGold = true
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("欢迎你,\(helper.user.nickname)")
Spacer()
Button("编辑昵称") {
router.push(.edit)
}
}
// 金币显示部分略...
}
.padding()
}
}
编辑页:修改昵称并刷新主视图
编辑页不需要通过参数传值,只需在内部使用 @ObservedObject 和 @EnvironmentObject 即可:
Swift
import Foundation
import SwiftUI
struct EditView: View {
@EnvironmentObject var router: RouterHelper
@ObservedObject var user = PHUserHelper.shared.user
@State private var input: String = ""
var body: some View {
VStack(spacing: 20) {
TextField("输入新昵称", text: $input)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("保存") {
user.nickname = input
router.pop() // 返回上一级页面
}
}
.padding()
.onAppear {
input = user.nickname
}
}
}
- @EnvironmentObject 适合用于整个 App 中的共享对象,如用户状态、导航器、设置等;
- 它无需显式传参,SwiftUI 会在视图树中查找对应类型的注入对象;
- 一旦数据变化,所有依赖它的视图都会自动刷新;
- 注意必须在上层注入 .environmentObject(...),否则会导致运行时崩溃。

场景 | 是否适合用@EnvironmentObject |
---|---|
管理全局导航逻辑 | ✅ 是 |
多个页面需要访问同一个用户对象 | ✅ 是 |
只在当前视图内部使用的数据 | ❌ 否,考虑 @State 或 @ObservedObject |
结语
SwiftUI 是一个高度响应式的框架,它的核心思想是数据驱动视图。只要状态发生变化,视图就会自动更新。为了支持这种机制,SwiftUI 提供了多种状态属性包装器,而其中最常见的三种就是我们今天讲解的:@State、@ObservedObject、@EnvironmentObject。
通过用户主页这一现实场景,我们看到了它们各自的使用姿势与适用范围。在实际开发中,理解它们的作用范围 、声明周期管理 和视图响应方式,可以帮助我们更高效地构建清晰、可靠、响应式的用户界面。
三种状态绑定方式对比表:
特性 | @State | @ObservedObject | @EnvironmentObject |
---|---|---|---|
生命周期归属 | 当前视图 | 外部传入的可观察对象 | 上层注入的共享对象 |
适用范围 | 小范围内部状态(局部 UI 控制) | 多视图间共享状态 | 跨层级/全局状态共享 |
数据变化后视图刷新 | ✅ 自动 | ✅ 自动(只刷新使用该属性的视图) | ✅ 自动(所有引用该对象的视图) |
声明时传入方式 | 本地初始化 | 需要从外部 init() 传入 | 必须通过 .environmentObject()注入 |
示例 | 控制按钮开关、输入框文本等 | 用户信息、定时器、下载状态等 | 路由器、主题管理器、全局配置等 |