SwiftUI 状态管理

从 @State 到最新的 Observation 框架,理解声明式 UI 如何通过数据流驱动视图的精准刷新。

深入理解 SwiftUI 的声明式 UI 范式和响应式数据流,构建现代化 iOS 应用界面。


1. SwiftUI 核心理念

SwiftUI 是 Apple 在 2019 年推出的声明式 UI 框架,它彻底改变了 iOS 开发的方式。

声明式 vs 命令式

// 命令式 (UIKit)
let label = UILabel()
label.text = "Hello"
label.textColor = .blue
view.addSubview(label)

// 声明式 (SwiftUI)
Text("Hello")
    .foregroundColor(.blue)

核心优势

  • 代码即界面:所见即所得的开发体验
  • 自动更新:状态变化自动触发 UI 刷新
  • 跨平台:一套代码适配 iOS、macOS、watchOS、tvOS
  • 实时预览:Xcode Preview 即时查看效果

2. 状态管理体系

SwiftUI 提供了多种属性包装器来管理不同场景的状态。

@State - 视图私有状态

用于视图内部的简单状态,值类型优先:

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("计数: \(count)")
                .font(.largeTitle)
            
            HStack(spacing: 40) {
                Button("减少") { count -= 1 }
                Button("增加") { count += 1 }
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

@Binding - 双向数据绑定

在父子视图间共享状态:

struct ParentView: View {
    @State private var isOn = false
    
    var body: some View {
        VStack {
            Text(isOn ? "开启" : "关闭")
            ToggleView(isOn: $isOn)  // 传递绑定
        }
    }
}

struct ToggleView: View {
    @Binding var isOn: Bool  // 接收绑定
    
    var body: some View {
        Toggle("开关", isOn: $isOn)
    }
}

@StateObject vs @ObservedObject

class UserViewModel: ObservableObject {
    @Published var name = ""
    @Published var isLoading = false
    
    func loadUser() async {
        isLoading = true
        defer { isLoading = false }
        // 加载用户数据...
    }
}

struct UserView: View {
    // ✅ 创建并拥有对象的生命周期
    @StateObject private var viewModel = UserViewModel()
    
    var body: some View {
        UserDetailView(viewModel: viewModel)
    }
}

struct UserDetailView: View {
    // ✅ 引用外部传入的对象
    @ObservedObject var viewModel: UserViewModel
    
    var body: some View {
        Text(viewModel.name)
    }
}

@EnvironmentObject - 跨层级共享

适合全局状态如主题、用户信息等:

class AppSettings: ObservableObject {
    @Published var isDarkMode = false
    @Published var fontSize: CGFloat = 16
}

// 在根视图注入
@main
struct MyApp: App {
    @StateObject private var settings = AppSettings()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(settings)
        }
    }
}

// 在任意子视图使用
struct SettingsView: View {
    @EnvironmentObject var settings: AppSettings
    
    var body: some View {
        Toggle("深色模式", isOn: $settings.isDarkMode)
    }
}

3. @Observable 宏 (iOS 17+)

iOS 17 引入的新观察机制,大幅简化代码:

// 旧方式 - ObservableObject
class OldViewModel: ObservableObject {
    @Published var name = ""
    @Published var email = ""
    @Published var age = 0
}

// 新方式 - @Observable
@Observable
class NewViewModel {
    var name = ""      // 自动追踪
    var email = ""     // 自动追踪
    var age = 0        // 自动追踪
}

struct ProfileView: View {
    var viewModel = NewViewModel()  // 无需 @StateObject
    
    var body: some View {
        Form {
            TextField("姓名", text: $viewModel.name)
            TextField("邮箱", text: $viewModel.email)
            Stepper("年龄: \(viewModel.age)", value: $viewModel.age)
        }
    }
}

@Observable 的优势

特性 ObservableObject @Observable
属性标记 每个需要 @Published 自动追踪所有属性
精确更新 任意属性变化都刷新 只刷新使用的属性
绑定语法 需要 $ 前缀 需要 @Bindable
嵌套观察 不支持 自动支持

4. 视图生命周期

onAppear / onDisappear

struct DataView: View {
    @State private var data: [Item] = []
    
    var body: some View {
        List(data) { item in
            Text(item.name)
        }
        .onAppear {
            loadData()
        }
        .onDisappear {
            cleanup()
        }
    }
}

task 修饰符

用于异步操作,自动处理取消:

struct AsyncView: View {
    @State private var user: User?
    
    var body: some View {
        Group {
            if let user {
                Text(user.name)
            } else {
                ProgressView()
            }
        }
        .task {
            // 视图消失时自动取消
            user = try? await fetchUser()
        }
        .task(id: userId) {
            // userId 变化时重新执行
            user = try? await fetchUser(id: userId)
        }
    }
}

5. 列表与导航

List 性能优化

struct OptimizedListView: View {
    @State private var items: [Item] = []
    
    var body: some View {
        List {
            ForEach(items) { item in
                ItemRow(item: item)
            }
            .onDelete(perform: deleteItems)
            .onMove(perform: moveItems)
        }
        .listStyle(.insetGrouped)
        .refreshable {
            await loadItems()
        }
        .searchable(text: $searchText)
    }
}
struct NavigationExample: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: item) {
                    Text(item.name)
                }
            }
            .navigationDestination(for: Item.self) { item in
                ItemDetailView(item: item)
            }
            .navigationDestination(for: Category.self) { category in
                CategoryView(category: category)
            }
            .navigationTitle("首页")
        }
    }
    
    // 编程式导航
    func navigateToItem(_ item: Item) {
        path.append(item)
    }
    
    func popToRoot() {
        path.removeLast(path.count)
    }
}

6. 自定义视图修饰符

ViewModifier 协议

struct CardModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.white)
            .cornerRadius(16)
            .shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 5)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardModifier())
    }
}

// 使用
Text("卡片内容")
    .cardStyle()

条件修饰符

extension View {
    @ViewBuilder
    func `if`<Transform: View>(
        _ condition: Bool,
        transform: (Self) -> Transform
    ) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

// 使用
Text("Hello")
    .if(isHighlighted) { view in
        view.foregroundColor(.red)
    }

7. 动画系统

隐式动画

struct AnimatedView: View {
    @State private var isExpanded = false
    
    var body: some View {
        VStack {
            Rectangle()
                .fill(.blue)
                .frame(width: isExpanded ? 200 : 100,
                       height: isExpanded ? 200 : 100)
                .animation(.spring(duration: 0.5), value: isExpanded)
            
            Button("切换") {
                isExpanded.toggle()
            }
        }
    }
}

显式动画

Button("动画") {
    withAnimation(.easeInOut(duration: 0.3)) {
        isExpanded.toggle()
    }
}

转场动画

struct TransitionView: View {
    @State private var showDetail = false
    
    var body: some View {
        VStack {
            if showDetail {
                DetailView()
                    .transition(.asymmetric(
                        insertion: .slide.combined(with: .opacity),
                        removal: .scale.combined(with: .opacity)
                    ))
            }
            
            Button("显示详情") {
                withAnimation {
                    showDetail.toggle()
                }
            }
        }
    }
}

8. 最佳实践

✅ 推荐做法

// 1. 保持视图简单,逻辑放入 ViewModel
struct GoodView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        content
            .task { await viewModel.load() }
    }
}

// 2. 使用 @ViewBuilder 提取复杂视图
@ViewBuilder
private var content: some View {
    if viewModel.isLoading {
        ProgressView()
    } else {
        dataView
    }
}

// 3. 合理拆分子视图
struct ItemRow: View {
    let item: Item
    var body: some View { /* ... */ }
}

❌ 避免的做法

// 1. 不要在 body 中执行副作用
var body: some View {
    // ❌ 错误
    let _ = print("body called")
    fetchData()  // ❌ 不要这样做
    
    Text("Hello")
}

// 2. 不要过度使用 @State
// ❌ 复杂对象应该用 @StateObject
@State private var viewModel = ComplexViewModel()

// 3. 不要在父视图创建 @ObservedObject
// ❌ 每次刷新都会重建
@ObservedObject var vm = ViewModel()

9. 调试技巧

打印视图更新

extension View {
    func debugPrint(_ value: Any) -> some View {
        #if DEBUG
        let _ = print(value)
        #endif
        return self
    }
}

// 使用
Text("Hello")
    .debugPrint("Text 视图更新了")

Self._printChanges()

var body: some View {
    let _ = Self._printChanges()  // 打印触发更新的属性
    Text("Hello")
}