4.1 什么是"状态"?为何需要状态管理?
在 SwiftUI 中,状态(State)是驱动界面渲染的数据。界面是状态的函数:View = f(State)。当状态发生变化时,SwiftUI 会自动重新计算并更新相关的视图——开发者无需手动调用 reloadData() 或 updateUI()。
"数据驱动 UI"。不要直接操作视图(命令式),而是修改数据,让框架决定如何更新界面(声明式)。这与 React、Vue 等前端框架的思想是一致的。
不同场景需要不同的状态管理工具:
| 工具 | 适用场景 | 数据范围 |
|---|---|---|
@State | 视图内部的局部状态 | 单个视图私有 |
@Binding | 父子视图间共享可写状态 | 两个视图之间 |
@StateObject | 视图拥有并管理的引用类型对象 | 视图及其子视图 |
@ObservedObject | 视图观察但不拥有的引用类型对象 | 视图及其子视图 |
@Observable | iOS 17+ 新式可观察对象 | 任意范围 |
@EnvironmentObject | 整个视图树共享的全局状态 | 整个应用 |
@Environment | 读取系统提供的环境值 | 系统环境 |
4.2 @State — 局部状态
@State 是最基础的状态管理工具。它让 SwiftUI 在视图结构体之外存储这个值(因为 SwiftUI 的 View 是值类型,struct 不能自我修改),并在值变化时自动触发视图重新渲染。
SwiftUI 运行时维护一份独立的存储,@State 变量实际存储在那里,而不是视图结构体本身。这就是为什么即使视图被重新创建,状态值也不会丢失。
struct CounterView: View {
// @State 声明局部状态,通常设为 private 防止外部访问
@State private var count = 0
@State private var isShowingAlert = false
@State private var inputText = ""
var body: some View {
VStack(spacing: 20) {
// 显示状态值
Text("计数:\(count)")
.font(.largeTitle)
.fontWeight(.bold)
HStack(spacing: 20) {
Button("-") {
count -= 1 // 修改状态 → 触发重渲染
}
.buttonStyle(.bordered)
Button("+") {
count += 1
}
.buttonStyle(.borderedProminent)
}
// TextField 双向绑定文本状态
TextField("输入内容", text: $inputText) // $ 前缀获取 Binding
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Text("你输入了:\(inputText)")
.foregroundColor(.secondary)
}
}
}
// @State 也可以用于复杂类型
struct FormView: View {
@State private var selectedColor = Color.blue
@State private var sliderValue: Double = 0.5
@State private var items: [String] = ["苹果", "香蕉"]
var body: some View {
VStack {
Slider(value: $sliderValue, in: 0...1)
ColorPicker("选择颜色", selection: $selectedColor)
Button("添加元素") {
items.append("新项目 \(items.count)") // 修改数组状态
}
}
}
}
4.3 @Binding — 双向绑定
@Binding 是一个对其他地方存储的状态的引用。子视图通过 @Binding 接收父视图的状态,读取并修改它,而实际数据存储在父视图中。这实现了"双向绑定"——子视图对数据的修改会直接反映到父视图。
// 子视图:接收 Binding,可以读写
struct ToggleRow: View {
let title: String
@Binding var isOn: Bool // 不是自己的状态,是对外部状态的引用
var body: some View {
HStack {
Text(title)
Spacer()
Toggle("", isOn: $isOn) // 再次用 $ 向下传递
}
.padding(.horizontal)
.onTapGesture {
isOn.toggle() // 直接修改,父视图也会更新
}
}
}
// 父视图:持有实际状态,用 $ 前缀创建 Binding 传入
struct SettingsView: View {
@State private var notificationsOn = true
@State private var darkModeOn = false
@State private var soundOn = true
var body: some View {
VStack {
ToggleRow(title: "通知", isOn: $notificationsOn)
ToggleRow(title: "深色模式", isOn: $darkModeOn)
ToggleRow(title: "声音", isOn: $soundOn)
// 在父视图中使用这些状态
if notificationsOn {
Text("通知已开启").foregroundColor(.green)
}
}
}
}
// 用 Binding.constant() 创建不可修改的 Binding(用于预览)
#Preview {
ToggleRow(title: "预览", isOn: .constant(true))
}
在 @State var count = 0 中,count 是 Int 类型的值,而 $count 是 Binding<Int> 类型的绑定。需要传递可写引用时用 $,需要只读值时直接用变量名。
4.4 @StateObject 与 @ObservableObject(经典模式)
当状态逻辑复杂,需要封装到单独的类中时,使用 ObservableObject 协议(iOS 14+)。这是 iOS 17 引入 @Observable 宏之前的主流方案。
ObservableObject — 可观察对象协议
遵循 ObservableObject 的类,其 @Published 属性变化时会通知所有观察它的视图。
// ViewModel:遵循 ObservableObject 协议
class CounterViewModel: ObservableObject {
// @Published:属性变化时自动通知观察者(视图)
@Published var count = 0
@Published var history: [Int] = []
var average: Double { // 计算属性,不触发通知
history.isEmpty ? 0 : Double(history.reduce(0, +)) / Double(history.count)
}
func increment() {
count += 1
history.append(count)
}
func reset() {
count = 0
history.removeAll()
}
}
// @StateObject:视图"拥有"这个对象,负责其生命周期
// 视图第一次渲染时创建,视图销毁时对象也销毁
struct CounterView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("计数:\(viewModel.count)")
Text("均值:\(viewModel.average, specifier: "%.1f")")
Button("+1") { viewModel.increment() }
Button("重置") { viewModel.reset() }
// 传给子视图时用 @ObservedObject
HistoryView(viewModel: viewModel)
}
}
}
// @ObservedObject:视图"观察"外部传入的对象,不管理其生命周期
struct HistoryView: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
List(viewModel.history, id: \.self) { value in
Text("历史值:\(value)")
}
}
}
关键区别在于所有权:@StateObject 负责创建和管理对象(视图树中有且仅有一个创建者),@ObservedObject 只是观察外部传入的对象。如果错误地用 @ObservedObject 创建对象,视图每次重建时对象都会被重新创建,导致状态丢失。
4.5 @Observable 宏(iOS 17 新模式)
iOS 17 引入了基于 Swift 5.9 宏系统的 @Observable,大幅简化了可观察对象的写法,并解决了 ObservableObject 的一些性能问题(精细追踪,只有被访问的属性发生变化才触发重渲染)。
① 不再需要 @Published,所有存储属性自动可观察;② 性能更好,精细追踪每个属性;③ 语法更简洁;④ 可用于结构体(未来支持);⑤ 与 @State 无缝配合,不再需要区分 @StateObject 和 @ObservedObject。
import Observation // iOS 17+,需要导入
// 旧方式(iOS 14+)
class OldUserStore: ObservableObject {
@Published var name = "小明"
@Published var age = 25
@Published var isLoggedIn = false
}
// 新方式(iOS 17+)—— 使用 @Observable 宏
@Observable
class UserStore {
var name = "小明" // 自动可观察,无需 @Published
var age = 25
var isLoggedIn = false
// 不想被观察的属性:加 @ObservationIgnored
@ObservationIgnored
var internalCache: [String: Any] = [:]
func login(name: String) {
self.name = name
self.isLoggedIn = true
}
func logout() {
isLoggedIn = false
}
}
// 在视图中使用 @Observable 对象
struct ProfileView: View {
// 视图拥有对象:用 @State(不再需要 @StateObject)
@State private var store = UserStore()
var body: some View {
VStack {
Text(store.name)
Text(store.isLoggedIn ? "已登录" : "未登录")
Button("登录") { store.login(name: "新用户") }
// 传入子视图:直接传,不需要 $ 也不需要 @ObservedObject
UserDetailView(store: store)
}
}
}
// 子视图接收 @Observable 对象
struct UserDetailView: View {
var store: UserStore // 普通属性,SwiftUI 自动追踪
var body: some View {
Text("年龄:\(store.age)")
}
}
// 如果需要 Binding(配合 TextField 等),使用 @Bindable
struct EditProfileView: View {
@Bindable var store: UserStore // @Bindable 让属性可以用 $ 创建 Binding
var body: some View {
TextField("姓名", text: $store.name) // $ 前缀获取 Binding
}
}
4.6 @EnvironmentObject — 全局共享状态
@EnvironmentObject 是解决"状态透传"问题的方案。当多层视图都需要访问同一个状态,但中间层不需要时,不必一层层手动传递,只需将对象注入环境即可。
// 全局状态对象(旧方式,仍广泛使用)
class AppState: ObservableObject {
@Published var currentUser: String? = nil
@Published var cartCount = 0
@Published var theme = "light"
}
// 在 App 入口注入环境
@main
struct MyApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState) // 注入到整个视图树
}
}
}
// 任意深度的子视图都可以直接使用
struct CartBadge: View {
@EnvironmentObject var appState: AppState // 无需手动传入
var body: some View {
Image(systemName: "cart")
.overlay(alignment: .topTrailing) {
if appState.cartCount > 0 {
Text("\(appState.cartCount)")
.font(.caption2)
.padding(4)
.background(.red)
.foregroundColor(.white)
.clipShape(Circle())
.offset(x: 8, y: -8)
}
}
}
}
// @Observable 版本:用 .environment() 注入(iOS 17+)
@Observable
class NewAppState {
var currentUser: String? = nil
var cartCount = 0
}
// 注入方式略有不同
ContentView()
.environment(newAppState) // 不是 .environmentObject,是 .environment
// 子视图接收方式
struct SomeView: View {
@Environment(NewAppState.self) var appState // 使用类型参数
var body: some View {
Text(appState.currentUser ?? "未登录")
}
}
4.7 @Environment — 读取系统环境值
@Environment 用于读取 SwiftUI 系统预置的环境值,如当前颜色模式、字体大小、语言方向等。
struct AdaptiveView: View {
// 读取系统颜色模式(深色/浅色)
@Environment(\.colorScheme) var colorScheme
// 读取用户首选的字体大小等级
@Environment(\.dynamicTypeSize) var dynamicTypeSize
// 读取语言阅读方向(LTR/RTL)
@Environment(\.layoutDirection) var layoutDirection
// 读取地区/语言
@Environment(\.locale) var locale
// dismiss 操作(关闭当前视图)
@Environment(\.dismiss) var dismiss
// openURL 操作(打开链接)
@Environment(\.openURL) var openURL
var body: some View {
VStack {
// 根据颜色模式切换样式
Text("当前模式:\(colorScheme == .dark ? "深色" : "浅色")")
.foregroundColor(colorScheme == .dark ? .white : .black)
.padding()
.background(colorScheme == .dark ? .black : .white)
.cornerRadius(8)
Button("关闭") {
dismiss() // 关闭当前 sheet 或导航页
}
Button("访问官网") {
openURL(URL(string: "https://developer.apple.com")!)
}
}
}
}
// 常用系统环境值
// \.colorScheme — .light / .dark
// \.dynamicTypeSize — 用户字体大小偏好
// \.locale — 语言地区
// \.timeZone — 时区
// \.dismiss — 关闭视图操作
// \.openURL — 打开 URL 操作
// \.isEnabled — 视图是否可交互
// \.editMode — List 编辑模式
// \.presentationMode — 视图展示模式(旧式,推荐用 dismiss)
4.8 单向数据流
SwiftUI 遵循单向数据流(Unidirectional Data Flow)原则:数据总是从父视图向子视图流动,子视图通过 Binding 或回调通知父视图修改数据。
AppState(全局)
↓ @EnvironmentObject / @Environment
RootView(@StateObject 拥有 ViewModel)
↓ 参数传递 / @Binding
ChildView A(@ObservedObject 观察 ViewModel)
↓ @Binding
ChildView B(@State 局部状态)
↑ 回调/Binding 通知父视图
4.9 实战示例:计数器 + 设置页面
综合运用本章所有状态管理工具,构建一个包含计数器和设置页面的完整示例。
// ─── 全局应用状态 ───
@Observable
class AppSettings {
var accentColor: Color = .blue
var stepSize: Int = 1
var showHistory: Bool = true
var maxCount: Int = 100
}
// ─── 计数器 ViewModel ───
@Observable
class CounterModel {
var count = 0
var history: [Int] = []
func increment(by step: Int, max: Int) {
guard count + step <= max else { return }
count += step
history.append(count)
}
func decrement(by step: Int) {
guard count - step >= 0 else { return }
count -= step
history.append(count)
}
func reset() {
count = 0
history.removeAll()
}
}
// ─── 设置子视图(使用 @Binding 接收可写状态)───
struct SettingsSheet: View {
@Bindable var settings: AppSettings
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationStack {
Form {
Section("计数设置") {
Stepper("步进值:\(settings.stepSize)",
value: $settings.stepSize, in: 1...10)
Stepper("最大值:\(settings.maxCount)",
value: $settings.maxCount, in: 10...1000, step: 10)
}
Section("显示设置") {
Toggle("显示历史记录", isOn: $settings.showHistory)
ColorPicker("主题色", selection: $settings.accentColor)
}
}
.navigationTitle("设置")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("完成") { dismiss() }
}
}
}
}
}
// ─── 主视图 ───
struct CounterRootView: View {
@State private var counter = CounterModel()
@State private var settings = AppSettings()
@State private var showSettings = false
var body: some View {
NavigationStack {
VStack(spacing: 32) {
// 计数显示
Text("\(counter.count)")
.font(.system(size: 80, weight: .bold, design: .rounded))
.foregroundColor(settings.accentColor)
// 操作按钮
HStack(spacing: 24) {
Button {
counter.decrement(by: settings.stepSize)
} label: {
Image(systemName: "minus.circle.fill")
.font(.system(size: 48))
.foregroundColor(settings.accentColor.opacity(0.7))
}
Button {
counter.increment(by: settings.stepSize, max: settings.maxCount)
} label: {
Image(systemName: "plus.circle.fill")
.font(.system(size: 48))
.foregroundColor(settings.accentColor)
}
}
Button("重置", role: .destructive) {
counter.reset()
}
// 历史记录
if settings.showHistory && !counter.history.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(counter.history.suffix(10), id: \.self) { val in
Text("\(val)")
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(settings.accentColor.opacity(0.1))
.cornerRadius(8)
}
}
.padding(.horizontal)
}
}
}
.navigationTitle("计数器")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showSettings = true
} label: {
Image(systemName: "gearshape.fill")
}
}
}
.sheet(isPresented: $showSettings) {
SettingsSheet(settings: settings) // 传入设置对象
}
}
}
}
本章系统学习了 SwiftUI 状态管理的完整工具链:@State 管理局部状态,@Binding 实现父子双向绑定,@StateObject/@ObservedObject 处理引用类型(旧方式),@Observable 宏是 iOS 17 的现代化方案,@EnvironmentObject/@Environment 解决全局状态共享问题。核心原则是单向数据流——数据从上到下,事件从下到上。