SwiftUI

Model Data

深入理解 SwiftUI 中的数据模型管理,包括 State、Binding、Observable、Environment 等核心概念及其使用方法。

SwiftUI 采用声明式的用户界面设计方法。当你构建视图层次结构时,也同时为视图指定了数据依赖关系。当数据发生变化时,无论是由于外部事件还是用户操作,SwiftUI 都会自动更新受影响的界面部分。

核心概念

SwiftUI 提供了多种工具来连接应用数据与用户界面,这些工具帮助你为应用中的每一份数据维护单一数据源(Single Source of Truth)。

数据流工具选择

  • State - 在视图内部管理临时 UI 状态,用于值类型
  • Binding - 共享数据源的引用,创建双向连接
  • Observable - 连接并观察引用类型的模型数据(iOS 17+)
  • Environment - 在视图层次结构中共享数据
  • StateObject / ObservedObject - 传统的可观察对象方式(iOS 17 之前)

State - 视图本地状态

State 是一个属性包装器,用于在视图内部存储可变的值类型数据。SwiftUI 管理其底层存储,当值改变时自动更新相关视图。

基本用法

struct PodcastPlayerView: View {
    @State private var isPlaying = false
    
    var body: some View {
        Button(action: {
            isPlaying.toggle()
        }) {
            Image(systemName: isPlaying ? "pause.circle" : "play.circle")
                .font(.largeTitle)
        }
    }
}

关键特性

  • 使用 private 限制作用域,确保状态封装在声明它的视图层次中
  • 适用于临时 UI 状态,如按钮高亮、筛选设置、当前选中项
  • 不应用于持久化存储,因为状态变量的生命周期与视图生命周期一致
  • 通过 wrappedValue 访问实际值,通过 projectedValue$ 前缀)获取绑定

动画支持

struct AnimatedButton: View {
    @State private var isPlaying = false
    
    var body: some View {
        Button(action: {
            withAnimation {
                isPlaying.toggle()
            }
        }) {
            Image(systemName: isPlaying ? "pause.circle" : "play.circle")
                .scaleEffect(isPlaying ? 1.5 : 1.0)
        }
    }
}

withAnimation 闭包中修改状态值时,SwiftUI 会对所有依赖该值的视图变化应用动画。

Binding - 双向数据绑定

Binding 创建对数据源的双向引用,允许视图读取和修改数据,而不拥有数据。

基本用法

struct EpisodeView: View {
    let episode: Episode
    @Binding var isFavorite: Bool
    
    var body: some View {
        Toggle("收藏", isOn: $isFavorite)
    }
}

struct ParentView: View {
    @State private var episode = Episode(title: "SwiftUI 入门", isFavorite: false)
    
    var body: some View {
        EpisodeView(episode: episode, isFavorite: $episode.isFavorite)
    }
}

创建 Binding

// 从 State 投影
@State private var text = ""
TextField("输入", text: $text)

// 自定义 getter 和 setter
Binding(
    get: { self.value },
    set: { self.value = $0 }
)

// 常量绑定
Binding.constant(true)

动态成员查找

struct Book {
    var title: String
    var author: String
}

struct BookEditor: View {
    @Binding var book: Book
    
    var body: some View {
        Form {
            TextField("标题", text: $book.title)
            TextField("作者", text: $book.author)
        }
    }
}

Observable - 现代观察机制

从 iOS 17 开始,SwiftUI 支持 Swift 的 Observation 框架,这是观察者模式的 Swift 原生实现。

基本用法

import Observation

@Observable
class Book {
    var title: String = ""
    var author: String = ""
    var isAvailable: Bool = true
    
    init(title: String, author: String) {
        self.title = title
        self.author = author
    }
}

在视图中使用

struct BookView: View {
    let book: Book
    
    var body: some View {
        VStack {
            Text(book.title)
            Text(book.author)
        }
    }
}

SwiftUI 自动追踪 body 中读取的属性。当 titleauthor 改变时视图更新,但 isAvailable 改变时不会触发更新。

忽略观察

@Observable
class DataModel {
    var observedProperty: String = ""
    
    @ObservationIgnored
    var ignoredProperty: Int = 0
}

全局和单例支持

@Observable
class AppSettings {
    static let shared = AppSettings()
    var theme: String = "light"
}

struct SettingsView: View {
    var body: some View {
        Text("当前主题: \(AppSettings.shared.theme)")
    }
}

即使不存储 observable 类型,SwiftUI 也能建立依赖追踪。

Bindable - 为 Observable 提供绑定

Bindable 属性包装器为 observable 类型的可变属性创建绑定支持。

基本用法

struct BookEditView: View {
    @Bindable var book: Book
    
    var body: some View {
        Form {
            TextField("标题", text: $book.title)
            TextField("作者", text: $book.author)
            Toggle("可借阅", isOn: $book.isAvailable)
        }
    }
}

局部变量绑定

struct LibraryView: View {
    let books: [Book]
    
    var body: some View {
        List(books) { book in
            @Bindable var book = book
            Toggle(book.title, isOn: $book.isAvailable)
        }
    }
}

直接修改 vs 绑定

对于简单的修改操作,可以直接修改属性:

Button("切换可用性") {
    book.isAvailable.toggle()
}

但当视图需要绑定时,必须使用 Bindable

Toggle("可用", isOn: $book.isAvailable)

Environment - 环境共享

Environment 允许在视图层次结构中共享数据,无需显式传递。

系统环境值

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {
            Text("当前模式: \(colorScheme == .dark ? "深色" : "浅色")")
            Button("关闭") {
                dismiss()
            }
        }
    }
}

自定义环境值

private struct ThemeKey: EnvironmentKey {
    static let defaultValue: String = "default"
}

extension EnvironmentValues {
    var theme: String {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

// 设置环境值
ContentView()
    .environment(\.theme, "dark")

// 读取环境值
struct ThemedView: View {
    @Environment(\.theme) var theme
    
    var body: some View {
        Text("主题: \(theme)")
    }
}

Observable 对象的环境共享

@Observable
class Library {
    var books: [Book] = []
}

struct LibraryApp: View {
    @State private var library = Library()
    
    var body: some View {
        BookListView()
            .environment(library)
    }
}

struct BookListView: View {
    @Environment(Library.self) private var library
    
    var body: some View {
        List(library.books) { book in
            Text(book.title)
        }
    }
}

StateObject 和 ObservedObject - 传统方式

这些是 iOS 17 之前使用 ObservableObject 协议的方式。

ObservableObject 协议

import Combine

class BookStore: ObservableObject {
    @Published var books: [Book] = []
    @Published var selectedBook: Book?
    
    // 不需要发布的属性
    let storeId: UUID = UUID()
}

StateObject - 创建和拥有

struct LibraryView: View {
    @StateObject private var store = BookStore()
    
    var body: some View {
        List(store.books) { book in
            Text(book.title)
        }
    }
}

StateObject 确保对象在视图的整个生命周期中只创建一次。

ObservedObject - 观察已有对象

struct BookDetailView: View {
    @ObservedObject var store: BookStore
    
    var body: some View {
        if let book = store.selectedBook {
            Text(book.title)
        }
    }
}

ObservedObject 用于观察从父视图传入的对象。

EnvironmentObject - 环境注入

struct RootView: View {
    @StateObject private var store = BookStore()
    
    var body: some View {
        NavigationStack {
            LibraryView()
        }
        .environmentObject(store)
    }
}

struct LibraryView: View {
    @EnvironmentObject var store: BookStore
    
    var body: some View {
        List(store.books) { book in
            Text(book.title)
        }
    }
}

从 ObservableObject 迁移到 Observable

迁移步骤

1. 更新数据模型

// 之前
class Book: ObservableObject {
    @Published var title: String = ""
    @Published var author: String = ""
    let id: UUID = UUID()
}

// 之后
@Observable
class Book {
    var title: String = ""
    var author: String = ""
    
    @ObservationIgnored
    let id: UUID = UUID()
}

2. 更新视图属性包装器

// 之前
struct LibraryView: View {
    @StateObject private var library = Library()
    @ObservedObject var book: Book
    @EnvironmentObject var settings: Settings
}

// 之后
struct LibraryView: View {
    @State private var library = Library()
    var book: Book  // 不需要包装器
    @Environment(Settings.self) var settings
}

3. 添加绑定支持

// 之前
struct BookEditor: View {
    @ObservedObject var book: Book
    
    var body: some View {
        TextField("标题", text: $book.title)
    }
}

// 之后
struct BookEditor: View {
    @Bindable var book: Book
    
    var body: some View {
        TextField("标题", text: $book.title)
    }
}

Observable 的优势

  • 更精确的更新 - 仅当视图 body 读取的属性改变时才更新
  • 支持可选类型和集合 - 可以追踪可选对象和对象集合
  • 简化的属性包装器 - 使用 StateEnvironment 而非专用的对象包装器
  • 更好的性能 - 减少不必要的视图更新

数据变化监听

onChange 修饰符

struct SearchView: View {
    @State private var searchText = ""
    
    var body: some View {
        TextField("搜索", text: $searchText)
            .onChange(of: searchText) { oldValue, newValue in
                print("搜索文本从 '\(oldValue)' 变为 '\(newValue)'")
                performSearch(newValue)
            }
    }
    
    func performSearch(_ query: String) {
        // 执行搜索逻辑
    }
}

初始值触发

TextField("搜索", text: $searchText)
    .onChange(of: searchText, initial: true) { oldValue, newValue in
        // 视图首次出现时也会调用
        performSearch(newValue)
    }

最佳实践

选择合适的工具

struct BestPracticesView: View {
    // ✅ 视图本地临时状态
    @State private var isExpanded = false
    
    // ✅ 视图拥有的模型数据(iOS 17+)
    @State private var book = Book()
    
    // ✅ 从父视图接收的绑定
    @Binding var selectedTab: Int
    
    // ✅ 环境中的共享数据
    @Environment(\.colorScheme) var colorScheme
    @Environment(AppSettings.self) var settings
    
    var body: some View {
        VStack {
            // 内容
        }
    }
}

状态作用域

// ✅ 好的做法 - 私有状态
struct GoodView: View {
    @State private var count = 0
}

// ❌ 避免 - 公开状态破坏封装
struct BadView: View {
    @State var count = 0  // 不应该是公开的
}

单一数据源

@Observable
class ShoppingCart {
    var items: [Item] = []
    
    var totalPrice: Double {
        items.reduce(0) { $0 + $1.price }
    }
    
    var itemCount: Int {
        items.count
    }
}

struct CartView: View {
    let cart: ShoppingCart
    
    var body: some View {
        VStack {
            Text("商品数量: \(cart.itemCount)")
            Text("总价: ¥\(cart.totalPrice, specifier: "%.2f")")
        }
    }
}

计算属性 totalPriceitemCountitems 派生,维护单一数据源。

避免过度发布

@Observable
class UserProfile {
    var name: String = ""
    var email: String = ""
    
    // ✅ 不需要观察的属性使用 @ObservationIgnored
    @ObservationIgnored
    var internalCache: [String: Any] = [:]
    
    @ObservationIgnored
    let createdAt: Date = Date()
}

性能优化

精确的依赖追踪

@Observable
class DataModel {
    var frequentlyChanging: Int = 0
    var rarelyChanging: String = ""
}

struct OptimizedView: View {
    let model: DataModel
    
    var body: some View {
        // 只读取 rarelyChanging,不会因 frequentlyChanging 改变而更新
        Text(model.rarelyChanging)
    }
}

视图拆分

// ❌ 整个视图会因任何属性改变而更新
struct MonolithicView: View {
    let book: Book
    
    var body: some View {
        VStack {
            Text(book.title)
            Text(book.author)
            Text("\(book.pageCount) 页")
            Text(book.publisher)
        }
    }
}

// ✅ 拆分为更小的视图,只在相关属性改变时更新
struct BookTitleView: View {
    let book: Book
    var body: some View { Text(book.title) }
}

struct BookAuthorView: View {
    let book: Book
    var body: some View { Text(book.author) }
}

struct OptimizedBookView: View {
    let book: Book
    
    var body: some View {
        VStack {
            BookTitleView(book: book)
            BookAuthorView(book: book)
        }
    }
}

常见模式

表单编辑

@Observable
class FormData {
    var username: String = ""
    var email: String = ""
    var agreedToTerms: Bool = false
}

struct RegistrationForm: View {
    @Bindable var formData: FormData
    
    var body: some View {
        Form {
            TextField("用户名", text: $formData.username)
            TextField("邮箱", text: $formData.email)
            Toggle("同意条款", isOn: $formData.agreedToTerms)
        }
    }
}

列表选择

struct BookListView: View {
    let books: [Book]
    @Binding var selectedBook: Book?
    
    var body: some View {
        List(books, selection: $selectedBook) { book in
            Text(book.title)
                .tag(book)
        }
    }
}

主从视图

@Observable
class AppState {
    var books: [Book] = []
    var selectedBook: Book?
}

struct MasterDetailView: View {
    @State private var appState = AppState()
    
    var body: some View {
        NavigationSplitView {
            List(appState.books, selection: $appState.selectedBook) { book in
                Text(book.title)
            }
        } detail: {
            if let book = appState.selectedBook {
                BookDetailView(book: book)
            } else {
                Text("选择一本书")
            }
        }
    }
}

总结

SwiftUI 的数据管理系统提供了强大而灵活的工具来保持 UI 与数据同步:

  • State 用于视图本地的临时状态
  • Binding 创建双向数据连接
  • Observable 是现代的观察机制(iOS 17+)
  • Bindable 为 observable 对象提供绑定支持
  • Environment 在视图层次中共享数据
  • StateObject/ObservedObject 是传统的可观察对象方式

选择合适的工具,遵循最佳实践,可以构建出高性能、易维护的 SwiftUI 应用。

在 GitHub 上编辑

上次更新于