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 中读取的属性。当 title 或 author 改变时视图更新,但 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读取的属性改变时才更新 - 支持可选类型和集合 - 可以追踪可选对象和对象集合
- 简化的属性包装器 - 使用
State和Environment而非专用的对象包装器 - 更好的性能 - 减少不必要的视图更新
数据变化监听
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")")
}
}
}计算属性 totalPrice 和 itemCount 从 items 派生,维护单一数据源。
避免过度发布
@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 应用。
上次更新于