这是一个将 @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, 和 @AppStorage 全部串联起来的完整示例。
场景模拟:一个简单的“用户仪表盘” App
在这个 App 中:
- 全局环境:有一个
UserSession(用户登录状态),全 App 共享。 - 本地设置:有一个“通知开关”,直接存入 UserDefaults。
- 独立页面逻辑:有一个计时器页面,拥有自己的 ViewModel。
- 父子传值:父页面控制弹窗,子页面修改父页面的标题。
1. 定义数据模型 (Model & ViewModel)
首先定义两个类,一个用于全局共享,一个用于特定页面。
import SwiftUI
import Combine
// 1. 全局共享对象 (类似于你的 SyncSettingsViewModel)
// 场景:需要在任何页面都能访问到的数据
class UserSession: ObservableObject {
@Published var username: String = "Guest"
@Published var isLoggedIn: Bool = false
}
// 2. 局部页面专用的 ViewModel
// 场景:只服务于“计时器”这个功能
class TimerViewModel: ObservableObject {
@Published var seconds: Int = 0
private var timer: AnyCancellable?
func start() {
if timer != nil { return }
timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in self.seconds += 1 }
}
func stop() {
timer?.cancel()
timer = nil
}
}
2. 视图层级 (Views)
请仔细看注释,每一处注解的选择都有其特定的理由。
// 入口:注入环境对象
struct MyApp: App {
// 创建唯一的全局实例
@StateObject private var userSession = UserSession()
var body: some Scene {
WindowGroup {
DashboardView()
.environmentObject(userSession) // 注入环境,之后所有子 View 都能用
}
}
}
// --- 主视图 ---
struct DashboardView: View {
// A. @EnvironmentObject: 从“空气”中抓取刚才注入的 session
@EnvironmentObject var session: UserSession
// B. @AppStorage: 自动读写 UserDefaults,持久化存储
@AppStorage("enableNotifications") var enableNotifs: Bool = true
// C. @State: 管理当前 View 的 UI 状态 (是否显示编辑弹窗)
@State private var showEditSheet = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Text("欢迎, \(session.username)")
.font(.largeTitle)
// 使用 AppStorage 的开关,重启 App 后状态依然保留
Toggle("接收通知", isOn: $enableNotifs)
.padding()
NavigationLink("进入计时器页面") {
// 跳转到子页面
TimerPageView()
}
Button("修改用户名") {
showEditSheet = true
}
}
.padding()
.sheet(isPresented: $showEditSheet) {
// 传递 Binding 给子视图
EditNameView(name: $session.username)
}
}
}
}
// --- 子视图 1:拥有独立逻辑的页面 ---
struct TimerPageView: View {
// D. @StateObject: 我是这个 ViewModel 的“主人”
// 这个 View 初始化时,创建 ViewModel。View 刷新时,它不会死。
@StateObject private var timerVM = TimerViewModel()
var body: some View {
VStack {
Text("计时: \(timerVM.seconds) 秒")
.font(.title)
HStack {
Button("开始") { timerVM.start() }
Button("停止") { timerVM.stop() }
}
Divider()
// 把 VM 传给更小的子组件
TimerDisplayView(vm: timerVM)
}
}
}
// --- 子视图 2:复用组件 ---
struct TimerDisplayView: View {
// E. @ObservedObject: 我只是“观察者”
// 我不创建它,我只接收父视图传下来的实例。
@ObservedObject var vm: TimerViewModel
var body: some View {
Text("子组件也在监听: \(vm.seconds)")
.foregroundColor(.gray)
.font(.caption)
}
}
// --- 子视图 3:修改弹窗 ---
struct EditNameView: View {
// F. @Binding: “传声筒”
// 我不存数据,我修改的是 DashboardView 里的 session.username
@Binding var name: String
@Environment(\.dismiss) var dismiss // 系统环境值,用于关闭弹窗
var body: some View {
VStack {
TextField("输入新名字", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Button("完成") {
dismiss()
}
}
.padding()
}
}
3. 代码逻辑大复盘
| 代码位置 | 使用的注解 | 为什么选它? | 这里的行为 |
|---|---|---|---|
| MyApp | @StateObject |
创建全局唯一的 UserSession 源头。 |
保证 App 生命周期内 session 活着。 |
| DashboardView | @EnvironmentObject |
UserSession 已经在 MyApp 注入了,这里直接取。 |
如果在其他页面改了名字,这里会自动更新。 |
| DashboardView | @AppStorage |
需要记住用户的“通知设置”,即使杀进程也要记住。 | 自动存取 UserDefaults 中的 "enableNotifications"。 |
| DashboardView | @State |
控制弹窗 (showEditSheet) 是纯 UI 逻辑,只属于当前页。 |
只有这个 View 关心弹窗是开是关。 |
| EditNameView | @Binding |
弹窗需要修改父视图的数据,但不需要拥有数据。 | 子视图输入框变动 -> 父视图数据变动。 |
| TimerPageView | @StateObject |
这个页面负责创建计时器逻辑 (TimerViewModel)。 |
页面存在时,计时器就存在;页面销毁,计时器销毁。 |
| TimerDisplayView | @ObservedObject |
它只负责显示,借用父视图创建好的 ViewModel。 | 跟着 TimerViewModel 的变化刷新 UI。 |
下一步建议
这个示例几乎涵盖了 95% 的 SwiftUI 开发场景。
如果你的项目是用 iOS 17 开发的,你还可以使用更现代的 Observation 框架(@Observable 宏),它把 @StateObject、@ObservedObject 和 @EnvironmentObject 的写法全部统一简化了。
你想看看如果用 iOS 17 的新写法,这段代码会简化成什么样吗?