SwiftData 教程:通过 Swift 简化数据持久化(二)
了解如何使用 @Model 和 @Attribute 来定义数据模型,并介绍 SwiftData 中的关系处理与 CRUD 操作。
上一篇文章之中,我们已经站在更高的层面理解了 SwiftData 的作用以及工作原理,如果你还没有看过不妨先看看:
在这篇文章,我们则会更加深度 SwiftData 的代码细节,瞧瞧在实际操作中如何进行使用。
使用 SwiftData 定义数据结构
@Model
:定义数据模型
SwiftData 的一个关键角色是@Model
宏,使用 @Model
对类进行简单的装饰,将类的存储属性转换为持久化属性。
以下是如何通过 SwiftData 创建表的简化示例:
@Model
class User {
var name: String
var gender: String
var birthday: Date
}
在这个例子中,@Model
注解将 User
类标记为数据模型,SwiftData 会根据这个类自动创建数据库表。
再来看一个稍微复杂一点的示例,假设我们有如下数据结构:
// Models/Habit.swift
import Foundation
enum HabitCategory: String, Codable {
case positive = "Positive"
case neutral = "Neutral"
case negative = "Negative"
}
struct Habit: Identifiable, Codable {
var id: UUID = UUID()
var name: String
var category: HabitCategory = .neutral // 默认为中性
var icon: String = "circle" // 默认使用圆圈图标
var description: String? // 可选
var createDate: Date = Date() // 创建时自动生成日期
var notes: String? // 可选
}
将上述数据模型转换成 SwiftData 模型:
import Foundation
import SwiftData
enum HabitCategory: String, Codable {
case positive = "Positive"
case neutral = "Neutral"
case negative = "Negative"
}
@Model
class Habit {
@Attribute(.unique) var id: String = UUID().uuidString
var name: String
var category: HabitCategory
var icon: String = "circle" // 默认使用圆圈图标
var summary: String? // 可选
var createDate: Date = Date() // 创建时自动生成日期
var notes: String? // 可选
init(
id: String = UUID().uuidString, name: String,
category: HabitCategory = .neutral, icon: String = "circle",
description: String? = nil, createDate: Date = Date(),
notes: String? = nil
) {
self.id = id
self.name = name
self.category = category
self.icon = icon
self.summary = description
self.createDate = createDate
self.notes = notes
}
}
你可能看到的几个明显变化:
- 需要将
struct
转成class
。因为struct
是值类型,每次更新都会创建数据的副本,而不是更新同一个对象。而 struct 是引用类型,不会有上述问题。 - 使用
@Model
标记的class
要求必须实现初始化方法(init()
)。
@Attribute
:定义属性行为
你可能还注意到一个新的宏——@Attribute()
。它是苹果在 SwiftData 中引入的一个新的宏,用于为数据模型中的属性指定一些特定的元数据。
为什么需要使用 @Attribute()
?
@Attribute 可以帮助你定义属性的行为,比如唯一性、可选性、默认值等。它简化了数据模型的配置,允许你通过 Swift 风格的语法直接在模型中定义这些属性的特性。
来看一个实际的对比例子。
假设我们有一个用户数据模型(User),其中包含唯一的用户 ID、必填的用户名和可选的电子邮件。如果不使用框架,我们可能会这样实现:
import Foundation
class User {
var id: UUID
var name: String
var email: String? // 可选
init(id: UUID = UUID(), name: String, email: String? = nil) {
self.id = id
self.name = name
self.email = email
}
}
class UserManager {
private var users: [User] = []
func addUser(name: String, email: String?) throws {
// 手动检查唯一性
if users.contains(where: { $0.name == name }) {
throw NSError(domain: "UserManager", code: 1, userInfo: [NSLocalizedDescriptionKey: "用户已存在"])
}
// 创建并保存用户
let newUser = User(name: name, email: email)
users.append(newUser)
}
func getUserById(id: UUID) -> User? {
return users.first { $0.id == id }
}
}
使用 SwiftData 后,我们可以将这些约束(如唯一性、必填项等)直接通过 @Attribute 注解实现,而不需要手动管理这些逻辑。
@Model
class User: Identifiable {
@Attribute(.unique) var id: UUID = UUID() // 唯一字段
@Attribute(.required) var name: String // 必填字段
@Attribute(.optional) var email: String? // 可选字段
}
这种通过 @Attribute(.required) 统一的声明方式,使代码更加清晰和直观。
常见的使用方式
- @Attribute(.unique)
用于标记一个属性为唯一的。例如,某个属性必须具有唯一性,不能重复。
常用于主键或其他需要唯一约束的字段。
- @Attribute(.required)
表示这个属性是必须的,不能为 nil。
类似于数据库中的 NOT NULL 约束。
- @Attribute(.optional)
表示这个属性是可选的,允许为 nil,是 Swift 中可选类型的补充标记。
- @Attribute(.indexed)
让这个属性被索引,便于快速查询。
- @Attribute(.default(someValue))
为属性定义一个默认值。
@Relationship
在应用开发中,数据模型往往不仅仅是孤立的个体,它们之间可能存在多种关系,例如一对一、一对多、多对多等。SwiftData 中的 @Relationship 即是用来定义数据模型之间的关系。
为什么使用 @Relationship
是推荐的?
- 明确关系:
@Relationship
属性可以明确地定义两个模型之间的关系,包括一对一、一对多、多对多等类型。它帮助 SwiftData 更好地管理和维护这些关系。 - 级联操作: 使用
@Relationship
可以设置级联删除(.cascade
),这意味着当一个模型实例被删除时,所有相关联的模型实例也会被自动删除。这对于管理复杂数据结构很有帮助。 - 数据一致性:
@Relationship
确保了关联数据的一致性。如果不使用@Relationship
,你需要手动管理数据的关联和一致性,这会增加错误的风险。
配置 ModelContainer
首先,你需要创建一个 ModelContainer,它会包含你的数据模型,这通常是在应用启动时进行的,比如在 App
中进行声明。
使用 let 创建一个 ModelContainer 常量,并在 init 初始化器中初始化,这是 App 推荐的做法:
import SwiftData
import SwiftUI
@main
struct HOMOApp: App {
// 创建 ModelContainer
private let modelContainer: ModelContainer
// 在初始器中管理 Habit 和 HabitGroup 模型
init() {
do {
let modelContainer = try ModelContainer(
for: Habit.self, HabitGroup.self)
self.modelContainer = modelContainer
} catch {
fatalError(error.localizedDescription)
}
}
var body: some Scene {
WindowGroup {
// ContentView()
MyHabitsTabView()
}
.modelContainer(modelContainer) // 将 modelContainer 注入 SwiftUI 环境中
}
}
使用 .modelContainer(container)
将 ModelContainer 注入到视图的环境中是为了让你的数据模型(在这个例子中是 Habit
和 HabitGroup
)能够在 SwiftUI 视图层级中无缝地被访问和操作。
注入到环境中后,所有需要访问 ModelContainer 和 ModelContext 的视图都可以通过 @Environment
或 @Query
属性包装器获取并操作数据,而不需要手动传递这些对象。
在这个例子中,ModelContainer 被注入到了 ContentView 的环境中,这意味着 ContentView 及其子视图可以通过 @Environment(\.modelContext)
访问到 ModelContext,并对数据进行增删改查等操作。
使用 ModelContext 进行 CRUD 操作
在配置好 ModelContainer 并将其注入到 SwiftUI 环境后,就可以通过ModelContext 来执行数据的增删改查操作。如我们在上一篇文章中讲到的,ModelContext 就类似于传统 ORM 中的Session
。
在 SwiftUI 中,每个需要进行数据库操作的视图都可以通过 @Environment(\.modelContext)
来获取 ModelContext。然后,你可以使用它执行数据库操作。
查询数据
如果一个 View 组件只是查询和展示数据,而不涉及插入、更新或删除等修改操作,那么并不需要使用 ModelContext。相反,你可以使用 @Query
属性包装器来轻松从数据库中获取数据,并在 SwiftUI 视图中展示。
@Query
会自动从 ModelContext 中提取数据,并根据数据模型的变化自动更新 UI,这正是 SwiftUI 声明是编程风格的体现。
- 只查询数据时:使用
@Query
即可,不需要使用 ModelContext。 - 进行数据操作(插入、更新、删除)时:需要使用 ModelContext。
举个简单的例子:
struct HabitListView: View {
@Query var habits: [Habit] // 使用 @Query 查询数据
var body: some View {
List(habits) { habit in
Text(habit.name)
}
}
}
在这个例子中,@Query
会负责数据的提取,并且自动监听数据的变化。如果数据模型发生更新,视图也会同步进行更新展示。
插入新数据
通过 ModelContext,插入新数据同样变得非常简单。你只需要实例化你的模型类,然后调用 ModelContext 的 .insert()
方法即可。
例如,向数据库中添加一个新的 Habit
实例:
struct AddHabitView: View {
@Environment(\.modelContext) private var modelContext
@State private var habitName: String = ""
var body: some View {
VStack {
TextField("Habit Name", text: $habitName)
Button("Add Habit") {
let newHabit = Habit(name: habitName)
modelContext.insert(newHabit)
}
}
}
}
在这个例子中,当用户点击按钮后,新的 Habit
实例会被创建并通过 modelContext.insert()
方法插入到数据库中。
删除数据
要删除某条记录,你可以使用 ModelContext 的.delete()
方法。通过提供模型对象作为参数,ModelContext
会自动处理删除操作。
例如,删除某个 Habit
实例:
struct HabitListView: View {
@Query var habits: [Habit]
@Environment(\.modelContext) private var modelContext
var body: some View {
List {
ForEach(habits) { habit in
Text(habit.name)
}
.onDelete { indexSet in
for index in indexSet {
let habitToDelete = habits[index]
modelContext.delete(habitToDelete)
}
}
}
}
}
这里,onDelete
事件被触发时,会从 habits
列表中删除对应的记录,并调用 modelContext.delete()
来执行实际的数据库删除操作。
更新数据
数据的更新可以通过直接修改模型实例的属性,然后调用 modelContext.save()
来持久化更改。
例如,更新 Habit
的名称:
struct UpdateHabitView: View {
@Environment(\.modelContext) private var modelContext
@State var habit: Habit
@State private var newName: String = ""
var body: some View {
VStack {
TextField("New Name", text: $newName)
Button("Update Habit") {
habit.name = newName
try? modelContext.save()
}
}
}
}
当用户点击按钮后,habit
实力的 name
属性被修改,随后 modelContext.save()
方法会保存这些更改到数据库。
保存数据
无论是插入、更新还是删除操作,最终你都需要通过 modelContext.save()
将这些更改持久化到数据库。save()
方法会捕获所有在 ModelContext
中的变更,并将其保存到底层的数据库中。
例如,保存所有的修改:
do {
try modelContext.save()
} catch {
// 错误处理逻辑
}
SwiftData 与 MVVM 架构
ModelContext
与 SwiftUI 的结合使得数据操作更加简洁且符合 SwiftUI 的声明式编程风格。开发者不再需要手动管理繁琐的事务逻辑,数据操作能够无缝地与 UI 绑定。此外,由于数据的响应式特性,数据的增删改查会自动反映在界面上,极大地简化了开发过程。
尽管 ModelContext
非常适合与 SwiftUI 结合使用,但需要注意的是,SwiftData 的设计理念是在视图中直接操作模型,因此在使用 MVVM 架构时可能会与这一设计产生冲突。如果你的项目大量依赖 MVVM 架构,需谨慎评估 SwiftData 的使用。
删除 Xcode SwiftData 数据缓存
在 Xcode 中调试的时候,我们有时候需要删除创建的 SwiftData 缓存数据。在 Xcode 中 Clean Build Folder 并不能清除 SwiftData 的数据。
我会创建一个 checkDatabaseLocation() 方法,使用这个方法来获取数据库的位置:
import Foundation
import SwiftData
func checkDatabaseLocation() {
if let urlApp = FileManager.default.urls(
for: .applicationSupportDirectory, in: .userDomainMask
).last {
let url = urlApp.appendingPathComponent("default.store")
if FileManager.default.fileExists(atPath: url.path) {
let path = url.path // 获取标准文件路径
print("SwiftData DB at \(path)")
} else {
print("SwiftData DB not found at \(url.path)")
}
} else {
print("Failed to locate application support directory.")
}
}
然后在我们的任意 View 文件中,通过 .onApper 来调用函数,此时终端中即可打印数据库文件地址:
.onAppear {
checkDatabaseLocation()
}
最后在终端中使用 rm 命令删除文件即可。
在接下来的文章中,我们将进一步探讨 SwiftData 的高级特性及其在复杂应用中的最佳实践。敬请期待!