第5章:基础状态管理

7 阅读6分钟

Snip20260416_2.png

示例代码都放这里啦,有需要的可以下载学习。swiftUIDemo

5.1 @State:本地视图状态

@State 介绍

@State 是 SwiftUI 中最基本的状态管理工具,用于管理视图的本地状态。它是一个属性包装器,允许我们在结构体中创建可变状态。

基本用法

import SwiftUI

struct CounterView: View {
    // 使用 @State 声明本地状态
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            // 显示状态值
            Text("Count: \(count)")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            // 修改状态
            Button("Increment") {
                count += 1  // 状态改变,UI 自动更新
            }
            .buttonStyle(.borderedProminent)
            
            Button("Reset") {
                count = 0  // 状态重置
            }
            .buttonStyle(.bordered)
        }
        .padding()
    }
}

#Preview {
    CounterView()
}

工作原理

@State 的工作原理:

  1. 当你使用 @State 标记一个属性时,SwiftUI 会在底层为这个属性创建一个独立的存储
  2. 这个存储不受结构体值类型特性的影响,即使结构体被重新创建,状态也会保持
  3. 当状态值改变时,SwiftUI 会自动重新计算视图的 body 属性
  4. 系统会对比新旧视图树,只更新发生变化的部分

最佳实践

  1. 标记为 private@State 应该只在当前视图内部使用,所以应该标记为 private
  2. 初始值:必须为 @State 属性提供初始值
  3. 避免在 body 中修改:不要在 body 计算属性中直接修改 @State
  4. 简单类型@State 适合存储简单类型(如 Bool、Int、String 等)

5.2 @Binding:父子视图双向绑定

@Binding 介绍

@Binding 用于在父子视图之间创建双向绑定,允许子视图修改父视图的状态。

基本用法

import SwiftUI

// 父视图
struct ParentView: View {
    // 父视图的状态
    @State private var isPlaying = false
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Parent View")
                .font(.headline)
            
            Text("Is Playing: \(isPlaying ? "Yes" : "No")")
            
            // 使用 $ 符号创建绑定并传递给子视图
            PlayButton(isPlaying: $isPlaying)
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

// 子视图
struct PlayButton: View {
    // 使用 @Binding 接收父视图的状态引用
    @Binding var isPlaying: Bool
    
    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            // 修改绑定值,会同步更新父视图的状态
            isPlaying.toggle()
        }
        .buttonStyle(.borderedProminent)
        .tint(isPlaying ? .red : .green)
        .padding()
        .background(Color.gray.opacity(0.05))
        .cornerRadius(8)
    }
}

#Preview {
    ParentView()
}

工作原理

@Binding 的工作原理:

  1. 它不是存储状态,而是创建一个对现有状态的引用
  2. 当子视图修改绑定值时,实际上是修改了原始的 @State 状态
  3. 状态的所有权仍然在父视图中
  4. 这种机制确保了单一数据源(SSOT)原则

实际应用

// 表单输入示例
struct FormView: View {
    @State private var username = ""
    @State private var email = ""
    
    var body: some View {
        VStack(spacing: 16) {
            Text("User Form")
                .font(.headline)
            
            TextFieldView(
                title: "Username",
                text: $username,
                placeholder: "Enter your username"
            )
            
            TextFieldView(
                title: "Email",
                text: $email,
                placeholder: "Enter your email"
            )
            
            Text("Username: \(username)")
            Text("Email: \(email)")
        }
        .padding()
    }
}

struct TextFieldView: View {
    let title: String
    @Binding var text: String
    let placeholder: String
    
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.subheadline)
                .fontWeight(.medium)
            
            TextField(
                placeholder,
                text: $text
            )
            .textFieldStyle(.roundedBorder)
        }
    }
}

5.3 @StateObject:可观察对象状态

@StateObject 介绍

@StateObject 用于管理符合 ObservableObject 协议的对象,适用于需要在多个视图之间共享的复杂状态。

基本用法

import SwiftUI
import Combine

// 可观察对象模型
class UserViewModel: ObservableObject {
    // 使用 @Published 标记需要发布的属性
    @Published var username = ""
    @Published var email = ""
    @Published var isLoggedIn = false
    
    func login() {
        // 模拟登录操作
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.isLoggedIn = true
        }
    }
    
    func logout() {
        username = ""
        email = ""
        isLoggedIn = false
    }
}

struct UserView: View {
    // 使用 @StateObject 管理可观察对象
    @StateObject private var viewModel = UserViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("User Profile")
                .font(.headline)
            
            if viewModel.isLoggedIn {
                Text("Welcome, \(viewModel.username)!")
                    .font(.title)
                Text("Email: \(viewModel.email)")
                
                Button("Logout") {
                    viewModel.logout()
                }
                .buttonStyle(.borderedProminent)
                .tint(.red)
            } else {
                TextField("Username", text: $viewModel.username)
                    .textFieldStyle(.roundedBorder)
                TextField("Email", text: $viewModel.email)
                    .textFieldStyle(.roundedBorder)
                
                Button("Login") {
                    viewModel.login()
                }
                .buttonStyle(.borderedProminent)
                .disabled(viewModel.username.isEmpty || viewModel.email.isEmpty)
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

#Preview {
    UserView()
}

工作原理

@StateObject 的工作原理:

  1. 它会创建并拥有一个符合 ObservableObject 协议的对象
  2. 当对象的 @Published 属性改变时,所有使用该对象的视图都会自动更新
  3. 即使视图被重新创建,@StateObject 也会保持对象的生命周期
  4. 适用于需要在多个视图之间共享的复杂状态

最佳实践

  1. 用于复杂状态:适用于包含多个相关属性的复杂状态
  2. 单一数据源:作为状态的唯一来源
  3. 生命周期管理:由 SwiftUI 管理对象的生命周期
  4. 性能考虑:对于大型对象,考虑使用更细粒度的状态管理

5.4 @ObservedObject:观察外部对象

@ObservedObject 介绍

@ObservedObject 用于观察外部传入的符合 ObservableObject 协议的对象,适用于从父视图传递的可观察对象。

基本用法

import SwiftUI

// 父视图
struct ParentWithObservedObject: View {
    // 父视图拥有状态对象
    @StateObject private var userViewModel = UserViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Parent View")
                .font(.headline)
            
            // 传递给子视图
            ChildView(viewModel: userViewModel)
        }
        .padding()
    }
}

// 子视图
struct ChildView: View {
    // 使用 @ObservedObject 观察外部对象
    @ObservedObject var viewModel: UserViewModel
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Child View")
                .font(.subheadline)
            
            TextField("Username", text: $viewModel.username)
                .textFieldStyle(.roundedBorder)
            
            TextField("Email", text: $viewModel.email)
                .textFieldStyle(.roundedBorder)
            
            Button("Login") {
                viewModel.login()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

#Preview {
    ParentWithObservedObject()
}

工作原理

@ObservedObject 的工作原理:

  1. 它不拥有对象,只是观察外部传入的对象
  2. 当对象的 @Published 属性改变时,视图会自动更新
  3. 对象的生命周期由其拥有者管理
  4. 适用于从父视图传递的可观察对象

与 @StateObject 的区别

特性@StateObject@ObservedObject
所有权拥有对象,管理生命周期观察对象,不管理生命周期
初始化在视图中直接初始化从外部传入
适用场景作为状态的唯一来源观察父视图传递的对象
性能更高效,避免重复创建可能会因父视图重建而重复创建

5.5 @EnvironmentObject:全局环境对象

@EnvironmentObject 介绍

@EnvironmentObject 用于访问通过环境传递的全局可观察对象,适用于跨多个视图层级共享的状态。

基本用法

import SwiftUI

// 全局状态模型
class AppState: ObservableObject {
    @Published var isDarkMode = false
    @Published var userLanguage = "zh"
    
    func toggleDarkMode() {
        isDarkMode.toggle()
    }
    
    func changeLanguage(to language: String) {
        userLanguage = language
    }
}

// 主视图 - 设置环境对象
struct MainView: View {
    @StateObject private var appState = AppState()
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Main View")
                    .font(.headline)
                
                NavigationLink("Settings", destination: SettingsView())
                NavigationLink("Profile", destination: ProfileView())
            }
            .padding()
        }
        // 通过环境传递对象
        .environmentObject(appState)
    }
}

// 设置视图 - 访问环境对象
struct SettingsView: View {
    // 通过 @EnvironmentObject 访问全局对象
    @EnvironmentObject private var appState: AppState
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Settings")
                .font(.headline)
            
            Toggle("Dark Mode", isOn: $appState.isDarkMode)
            
            Picker("Language", selection: $appState.userLanguage) {
                Text("English").tag("en")
                Text("中文").tag("zh")
                Text("日本語").tag("ja")
            }
            .pickerStyle(.segmented)
        }
        .padding()
        .background(appState.isDarkMode ? Color.black : Color.white)
        .foregroundColor(appState.isDarkMode ? Color.white : Color.black)
    }
}

// 个人资料视图 - 访问环境对象
struct ProfileView: View {
    @EnvironmentObject private var appState: AppState
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Profile")
                .font(.headline)
            
            Text("Current Language: \(appState.userLanguage)")
            Text("Dark Mode: \(appState.isDarkMode ? "On" : "Off")")
        }
        .padding()
        .background(appState.isDarkMode ? Color.black : Color.white)
        .foregroundColor(appState.isDarkMode ? Color.white : Color.black)
    }
}

#Preview {
    MainView()
}

工作原理

@EnvironmentObject 的工作原理:

  1. 它从环境中查找指定类型的可观察对象
  2. 当对象的 @Published 属性改变时,所有使用该对象的视图都会自动更新
  3. 不需要手动传递对象,通过环境自动注入
  4. 适用于跨多个视图层级共享的全局状态

最佳实践

  1. 全局状态:用于应用级别的全局状态
  2. 依赖注入:通过环境进行依赖注入,避免层层传递
  3. 类型安全:基于类型查找,确保类型正确
  4. 错误处理:确保在使用前在环境中设置了对象

5.6 @Environment:环境值

@Environment 介绍

@Environment 用于访问 SwiftUI 环境中的系统值,如布局方向、颜色方案、字体大小等。

基本用法

import SwiftUI

struct EnvironmentValuesView: View {
    // 访问环境值
    @Environment(\.colorScheme) private var colorScheme
    @Environment(\.layoutDirection) private var layoutDirection
    @Environment(\.dynamicTypeSize) private var dynamicTypeSize
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    
    var body: some View {
        VStack(spacing: 16) {
            Text("Environment Values")
                .font(.headline)
            
            Text("Color Scheme: \(colorScheme == .dark ? "Dark" : "Light")")
            Text("Layout Direction: \(layoutDirection == .leftToRight ? "LTR" : "RTL")")
            Text("Dynamic Type Size: \(dynamicTypeSize.description)")
            Text("Horizontal Size Class: \(horizontalSizeClass == .regular ? "Regular" : "Compact")")
            
            // 根据环境值调整布局
            if horizontalSizeClass == .regular {
                HStack {
                    Text("Wide Layout")
                    Spacer()
                    Text("More Content")
                }
            } else {
                VStack {
                    Text("Narrow Layout")
                    Text("Content Below")
                }
            }
        }
        .padding()
        .background(colorScheme == .dark ? Color.black : Color.white)
        .foregroundColor(colorScheme == .dark ? Color.white : Color.black)
    }
}

#Preview {
    EnvironmentValuesView()
}

常用环境值

环境值类型描述
\.colorSchemeColorScheme当前颜色方案(浅色/深色)
\.layoutDirectionLayoutDirection布局方向(LTR/RTL)
\.dynamicTypeSizeDynamicTypeSize动态字体大小
\.horizontalSizeClassUserInterfaceSizeClass?水平尺寸类
\.verticalSizeClassUserInterfaceSizeClass?垂直尺寸类
\.localeLocale当前区域设置
\.calendarCalendar当前日历
\.timeZoneTimeZone当前时区
\.accessibilityEnabledBool是否启用辅助功能
\.scenePhaseScenePhase场景阶段(活跃/非活跃/背景)

工作原理

@Environment 的工作原理:

  1. 它从 SwiftUI 环境中读取指定的环境值
  2. 当环境值改变时,视图会自动更新
  3. 环境值由系统或父视图设置
  4. 适用于响应系统设置和环境变化

5.7 @SceneStorage:场景存储

@SceneStorage 介绍

@SceneStorage 用于在场景级别持久化存储简单数据,适用于保存用户偏好设置和状态。

基本用法

import SwiftUI

struct SceneStorageView: View {
    // 使用 @SceneStorage 存储数据
    @SceneStorage("username") private var username = ""
    @SceneStorage("isDarkMode") private var isDarkMode = false
    @SceneStorage("counter") private var counter = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Scene Storage")
                .font(.headline)
            
            TextField("Username", text: $username)
                .textFieldStyle(.roundedBorder)
            
            Toggle("Dark Mode", isOn: $isDarkMode)
            
            VStack {
                Text("Counter: \(counter)")
                HStack {
                    Button("Increment") {
                        counter += 1
                    }
                    .buttonStyle(.bordered)
                    
                    Button("Reset") {
                        counter = 0
                    }
                    .buttonStyle(.bordered)
                }
            }
            
            Text("Note: Data persists across app restarts")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
        .background(isDarkMode ? Color.black : Color.white)
        .foregroundColor(isDarkMode ? Color.white : Color.black)
    }
}

#Preview {
    SceneStorageView()
}

工作原理

@SceneStorage 的工作原理:

  1. 它将数据存储在场景的 UserDefaults 中
  2. 数据会在场景重启后保持
  3. 每个场景有自己的存储,不同场景之间数据隔离
  4. 适用于存储用户偏好设置和临时状态

最佳实践

  1. 简单数据:适合存储简单类型(String、Int、Bool 等)
  2. 场景隔离:每个场景有独立的存储
  3. 自动持久化:数据自动保存,无需手动管理
  4. 键名唯一性:使用唯一的键名避免冲突

5.8 @AppStorage:应用存储

@AppStorage 介绍

@AppStorage 用于在应用级别持久化存储简单数据,适用于保存全局用户偏好设置。

基本用法

import SwiftUI

struct AppStorageView: View {
    // 使用 @AppStorage 存储数据
    @AppStorage("userName") private var userName = "Guest"
    @AppStorage("appTheme") private var appTheme = "light"
    @AppStorage("notificationsEnabled") private var notificationsEnabled = true
    
    var body: some View {
        VStack(spacing: 20) {
            Text("App Storage")
                .font(.headline)
            
            TextField("User Name", text: $userName)
                .textFieldStyle(.roundedBorder)
            
            Picker("Theme", selection: $appTheme) {
                Text("Light").tag("light")
                Text("Dark").tag("dark")
                Text("Auto").tag("auto")
            }
            .pickerStyle(.segmented)
            
            Toggle("Enable Notifications", isOn: $notificationsEnabled)
            
            Text("Note: Data persists across app reinstalls")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
        .background(getThemeColor())
        .foregroundColor(appTheme == "dark" ? Color.white : Color.black)
    }
    
    private func getThemeColor() -> Color {
        switch appTheme {
        case "dark":
            return Color.black
        case "light":
            return Color.white
        default:
            return Color.white
        }
    }
}

#Preview {
    AppStorageView()
}

工作原理

@AppStorage 的工作原理:

  1. 它将数据存储在应用的 UserDefaults 中
  2. 数据会在应用重启后保持
  3. 所有场景共享相同的存储
  4. 适用于存储全局用户偏好设置

与 @SceneStorage 的区别

特性@AppStorage@SceneStorage
存储范围应用级别,所有场景共享场景级别,每个场景独立
持久化持久化到 UserDefaults持久化到场景的 UserDefaults
适用场景全局偏好设置场景特定状态
数据共享跨场景共享场景隔离

5.9 @FocusedValue:聚焦值

@FocusedValue 介绍

@FocusedValue 用于在视图层次结构中传递聚焦状态相关的值,适用于处理键盘焦点和上下文相关操作。

基本用法

import SwiftUI

// 定义聚焦值键
struct EditModeKey: FocusedValueKey {
    typealias Value = Bool
}

// 扩展 FocusedValues
extension FocusedValues {
    var isEditMode: EditModeKey.Value? {
        get { self[EditModeKey.self] }
        set { self[EditModeKey.self] = newValue }
    }
}

struct FocusedValueView: View {
    @State private var isEditMode = false
    @State private var text = "Hello, SwiftUI"
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Focused Value")
                .font(.headline)
            
            // 设置聚焦值
            TextField("Enter text", text: $text)
                .textFieldStyle(.roundedBorder)
                .focusedValue(\.isEditMode, true)
            
            Button("Toggle Edit Mode") {
                isEditMode.toggle()
            }
            .buttonStyle(.borderedProminent)
            
            // 子视图访问聚焦值
            FocusedChildView()
        }
        .padding()
        .environment(\.isEditMode, isEditMode)
    }
}

struct FocusedChildView: View {
    // 访问聚焦值
    @FocusedValue(\.isEditMode) private var isEditMode
    
    var body: some View {
        VStack {
            Text("Child View")
                .font(.subheadline)
            
            Text("Edit Mode: \(isEditMode ?? false ? "On" : "Off")")
            
            if isEditMode ?? false {
                Text("Editing is enabled!")
                    .foregroundColor(.green)
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

#Preview {
    FocusedValueView()
}

工作原理

@FocusedValue 的工作原理:

  1. 它通过 FocusedValues 字典传递值
  2. 当焦点变化时,聚焦值会自动更新
  3. 适用于与焦点相关的上下文信息
  4. 可以自定义聚焦值键

适用场景

  1. 键盘焦点:跟踪当前聚焦的视图
  2. 上下文操作:根据聚焦状态显示不同的操作
  3. 编辑模式:在编辑模式下显示额外的控件
  4. 工具栏配置:根据当前聚焦的内容配置工具栏

5.10 状态驱动 UI 更新原理

核心原理

SwiftUI 的核心设计哲学是状态驱动

UI = f(State)

这意味着:

  1. UI 是状态的函数
  2. 当状态改变时,UI 会自动更新
  3. 给定相同的状态,总是渲染相同的 UI

更新流程

当状态改变时,SwiftUI 的更新流程如下:

  1. 状态改变:用户操作或其他因素导致状态值发生变化
  2. 检测变化:SwiftUI 检测到状态变化
  3. 重新计算 body:重新调用受影响视图的 body 计算属性
  4. 构建新视图树:生成新的视图层次结构
  5. 对比差异:对比新旧视图树,找出变化的部分
  6. 更新 UI:只更新发生变化的部分,保持其他部分不变

性能优化

SwiftUI 的状态驱动机制本身就很高效,因为:

  1. 增量更新:只更新变化的部分
  2. 值类型:视图是轻量级的值类型,创建成本低
  3. 智能对比:使用高效的差异算法
  4. 批处理:合并多个状态更新,减少渲染次数
  5. 懒加载:只渲染可见的部分

5.11 状态管理最佳实践

1. 选择合适的状态管理工具

状态类型推荐工具适用场景
本地简单状态@State单个视图的内部状态
父子视图共享@Binding子视图需要修改父视图状态
复杂对象状态@StateObject多个属性的复杂状态
外部对象引用@ObservedObject观察父视图传递的对象
全局共享状态@EnvironmentObject跨多个视图的全局状态
系统环境值@Environment访问系统设置和环境
场景级持久化@SceneStorage场景特定的持久状态
应用级持久化@AppStorage全局用户偏好设置
聚焦相关状态@FocusedValue与焦点相关的上下文信息

2. 状态管理原则

  1. 单一数据源:每个状态应该有唯一的来源
  2. 状态提升:将状态提升到需要访问它的所有视图的共同父视图
  3. 最小化状态:只存储必要的状态,避免冗余
  4. 状态隔离:将相关状态组织在一起,避免混乱
  5. 可测试性:状态管理应该易于测试
  6. 性能考虑:对于大型状态,考虑使用更细粒度的更新

3. 性能优化技巧

  1. 使用 Equatable:为模型实现 Equatable 协议,避免不必要的更新
  2. 视图分离:将复杂视图拆分为更小的子视图
  3. @State 用于简单类型@State 适合存储简单类型,复杂类型使用 @StateObject
  4. 避免在 body 中创建对象:不要在 body 计算属性中创建新对象
  5. 使用 .id() 强制更新:当需要强制视图更新时使用
  6. 考虑使用 Combine:对于复杂的异步操作,使用 Combine 框架

实战:创建一个完整的状态管理示例

需求分析

创建一个包含多种状态管理技术的应用,包括:

  1. 本地状态管理
  2. 父子视图绑定
  3. 可观察对象
  4. 环境对象
  5. 持久化存储

代码实现

import SwiftUI
import Combine

// 全局应用状态
class AppState: ObservableObject {
    @Published var isDarkMode = false
    @Published var currentUser: User? = nil
    
    func toggleTheme() {
        isDarkMode.toggle()
    }
    
    func login(user: User) {
        currentUser = user
    }
    
    func logout() {
        currentUser = nil
    }
}

// 用户模型
struct User: Identifiable, Equatable {
    let id = UUID()
    let name: String
    let email: String
}

// 主应用视图
struct StateManagementDemo: View {
    @StateObject private var appState = AppState()
    @AppStorage("lastLoggedInUser") private var lastLoggedInUser = ""
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                Text("状态管理演示")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                
                // 主题切换
                Toggle("深色模式", isOn: $appState.isDarkMode)
                
                // 用户登录状态
                if appState.currentUser != nil {
                    Text("欢迎, \(appState.currentUser?.name ?? "")!")
                    Button("退出登录") {
                        appState.logout()
                        lastLoggedInUser = ""
                    }
                    .buttonStyle(.borderedProminent)
                    .tint(.red)
                } else {
                    LoginView()
                }
                
                // 导航链接
                NavigationLink("计数器示例", destination: CounterView())
                NavigationLink("待办事项示例", destination: TodoApp())
                NavigationLink("环境对象示例", destination: EnvironmentObjectDemo())
            }
            .padding()
        }
        .environmentObject(appState)
        .preferredColorScheme(appState.isDarkMode ? .dark : .light)
    }
}

// 登录视图
struct LoginView: View {
    @State private var name = ""
    @State private var email = ""
    @EnvironmentObject private var appState: AppState
    @AppStorage("lastLoggedInUser") private var lastLoggedInUser = ""
    
    var body: some View {
        VStack(spacing: 16) {
            TextField("姓名", text: $name)
                .textFieldStyle(.roundedBorder)
            TextField("邮箱", text: $email)
                .textFieldStyle(.roundedBorder)
            Button("登录") {
                let user = User(name: name, email: email)
                appState.login(user: user)
                lastLoggedInUser = name
            }
            .buttonStyle(.borderedProminent)
            .disabled(name.isEmpty || email.isEmpty)
            
            if !lastLoggedInUser.isEmpty {
                Text("上次登录: \(lastLoggedInUser)")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
    }
}

// 计数器视图
struct CounterView: View {
    @State private var count = 0
    @SceneStorage("counterValue") private var storedCount = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("计数器")
                .font(.headline)
            Text("Count: \(count)")
                .font(.largeTitle)
                .fontWeight(.bold)
            HStack(spacing: 16) {
                Button("减1") {
                    count -= 1
                    storedCount = count
                }
                .buttonStyle(.bordered)
                Button("重置") {
                    count = 0
                    storedCount = 0
                }
                .buttonStyle(.bordered)
                Button("加1") {
                    count += 1
                    storedCount = count
                }
                .buttonStyle(.borderedProminent)
            }
            Text("场景存储值: \(storedCount)")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .padding()
        .onAppear {
            // 从场景存储恢复
            count = storedCount
        }
    }
}

// 待办事项应用
struct TodoApp: View {
    @State private var todos: [TodoItem] = [
        TodoItem(title: "学习 SwiftUI 状态管理"),
        TodoItem(title: "完成本章练习"),
        TodoItem(title: "构建示例应用")
    ]
    @State private var newTodoTitle = ""
    
    var body: some View {
        VStack {
            HStack(spacing: 8) {
                TextField("输入新的待办事项", text: $newTodoTitle)
                    .textFieldStyle(.roundedBorder)
                Button("添加") {
                    addTodo()
                }
                .buttonStyle(.borderedProminent)
                .disabled(newTodoTitle.isEmpty)
            }
            .padding()
            List {
                ForEach($todos) { $todo in
                    HStack {
                        Button(action: {
                            todo.isCompleted.toggle()
                        }) {
                            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundColor(todo.isCompleted ? .green : .gray)
                        }
                        .buttonStyle(.plain)
                        Text(todo.title)
                            .strikethrough(todo.isCompleted, color: .gray)
                            .foregroundColor(todo.isCompleted ? .secondary : .primary)
                        Spacer()
                        Button(action: {
                            deleteTodo(todo)
                        }) {
                            Image(systemName: "trash.fill")
                                .foregroundColor(.red)
                        }
                        .buttonStyle(.plain)
                    }
                }
            }
        }
        .navigationTitle("待办事项")
    }
    
    private func addTodo() {
        guard !newTodoTitle.isEmpty else { return }
        todos.append(TodoItem(title: newTodoTitle))
        newTodoTitle = ""
    }
    
    private func deleteTodo(_ todo: TodoItem) {
        if let index = todos.firstIndex(where: { $0.id == todo.id }) {
            todos.remove(at: index)
        }
    }
}

// 待办事项模型
struct TodoItem: Identifiable, Equatable {
    let id = UUID()
    var title: String
    var isCompleted = false
}

// 环境对象演示
struct EnvironmentObjectDemo: View {
    @EnvironmentObject private var appState: AppState
    
    var body: some View {
        VStack(spacing: 20) {
            Text("环境对象演示")
                .font(.headline)
            Text("当前主题: \(appState.isDarkMode ? "深色" : "浅色")")
            Text("登录状态: \(appState.currentUser != nil ? "已登录" : "未登录")")
            if let user = appState.currentUser {
                Text("用户: \(user.name)")
                Text("邮箱: \(user.email)")
            }
            Button("切换主题") {
                appState.toggleTheme()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

#Preview {
    StateManagementDemo()
}

代码解析

  1. AppState:使用 @StateObject 管理全局应用状态
  2. User:符合 IdentifiableEquatable 协议的用户模型
  3. StateManagementDemo:主应用视图,设置环境对象
  4. LoginView:使用 @State@AppStorage 管理登录状态
  5. CounterView:使用 @State@SceneStorage 管理计数器
  6. TodoApp:使用 @State 管理待办事项列表
  7. EnvironmentObjectDemo:使用 @EnvironmentObject 访问全局状态

小结

本章详细介绍了 SwiftUI 中的状态管理系统,包括:

  • @State:用于管理视图的本地状态
  • @Binding:用于父子视图之间的双向绑定
  • @StateObject:用于管理可观察对象的状态
  • @ObservedObject:用于观察外部传入的可观察对象
  • @EnvironmentObject:用于访问全局环境对象
  • @Environment:用于访问系统环境值
  • @SceneStorage:用于场景级别的持久化存储
  • @AppStorage:用于应用级别的持久化存储
  • @FocusedValue:用于传递聚焦相关的值
  • 状态驱动 UI 更新的原理
  • 状态管理的最佳实践
  • 一个完整的状态管理示例应用

通过本章的学习,你已经掌握了 SwiftUI 中所有的状态管理技术,能够根据不同的场景选择合适的状态管理工具,创建具有复杂交互功能的应用。


参考资料


本内容为《SwiftUI 基础教程》第五章,欢迎关注后续更新。