概述
数据持久性是所有应用程序都应该具备的重要功能,以便保存重要的数据,如用于快速加载的最新饲料、用户偏好和服务器关键信息。为了避免丢失数据和提供不一致的体验,正确管理本地数据是至关重要的。
在这篇文章中,我们将学习如何使用Realm作为SwiftUI的数据持久化引擎,以简单的方式管理iOS应用程序中的数据。
我们将创建具有以下功能的待办事项应用程序。
请注意,本教程使用的是Xcode 12、Swift v5和iOS 14。
为什么是Realm?
在我们开始开发之前,让我们看一下使用Realm的主要原因,你会从中受益。
- 具有面向对象的数据模型的轻量级移动数据库--不需要ORM
- 使用简单 - 你将花费更少的时间来设置Realm,编写查询,创建嵌套对象等。
- 通过全面的文档和广泛的社区支持,易于学习
- 支持多平台,更容易实现跨平台的数据库结构同步
设置您的SwiftUI项目
打开Xcode并创建一个新的SwiftUI项目。
安装 Realm SDK
在 Xcode 菜单中,转到文件>Swift 包>添加包依赖,并输入 Realm 仓库的 URL,如下图所示。
https://github.com/realm/realm-cocoa
点击下一步,它将重定向到这个屏幕。这个包的最新版本是v10.15.1。
在这个屏幕上,选中Realm和RealmSwift两个包。

创建一个待办事项模型
让我们用Identifiable 协议创建一个名为Task的待办事项模型。
struct Task: Identifiable {
var id: String
var title: String
var completed: Bool = false
var completedAt: Date = Date()
}
创建主列表视图
在本节中,我们将创建一个列表视图和可重用的项目视图。
TaskRowView
添加一个新的SwiftUIView 文件,名为TaskRowView ,并用以下代码进行更新。
struct TaskRowView: View {
// 1
let task: Task
var body: some View {
// 2
HStack(spacing: 8) {
Button(action: {
// mark complete action
}) {
Image(systemName: task.completed ? "checkmark.circle.fill" : "circle")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(task.completed ? Color.green : Color.gray)
}
Text(task.title)
.foregroundColor(.black)
Spacer()
}
.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20))
}
}
下面是上面写的代码的细节。
task是一个视图依赖数据,在视图初始化时需要。- 视图中包含一个标记任务完成状态的
Button,以及一个用于任务标题的Text,这些都在水平堆栈中管理。
创建任务列表视图
在这个项目中,我将使用LazyVStack 与ScrollView 。LazyVStack只适用于iOS v14及以上版本,但被称为列出项目的伟大SwiftUI组件之一。
最初,在与Realm整合之前,我们将使用样本待办事项数据。
创建一个名为TaskListView 的新文件来显示待办事项列表。
struct TaskListView: View {
// 1
private var mockTasks = [
Task(id: "001", title: "Eat Burger"),
Task(id: "002", title: "Go Swimming with Fred"),
Task(id: "003", title: "Make a Coffee"),
Task(id: "004", title: "Travel to Europe"),
]
var body: some View {
ScrollView {
LazyVStack (alignment: .leading) {
// 2
ForEach(mockTasks, id: \.id) { task in
// 3
TaskRowView(task: task)
Divider().padding(.leading, 20)
}
.animation(.default)
}
}
}
}
下面是我们上面所写的细节。
- 正如你所看到的,在我们与Realm数据库整合之前,会使用一些模拟数据。
TaskRowView在ForEach闭包中被调用,以显示每个mockTasks的项目- 最后,我们将
task对象传递给TaskRowView。
更新ContentView
一旦我们完成了这两个与任务相关的视图的创建,我们需要更新主ContentView 文件以包括NavigationView 和新创建的TaskListView 。下面的代码还将添加一个导航标题。
struct ContentView: View {
var body: some View {
NavigationView {
TaskListView()
.navigationTitle("Todo")
.navigationBarTitleDisplayMode(.automatic)
}
}
}
现在,如果我们尝试运行该项目,它将显示与下面类似的输出。

很好,我们已经为主要的待办事项列表创建了一个视图。现在,让我们在列表中添加一个简单的表单,使我们能够动态地添加更多的任务。
用添加新任务AddTaskView
创建一个新的view 文件,名为AddTaskView ,并用下面的代码更新它。
struct AddTaskView: View {
// 1
@State private var taskTitle: String = ""
var body: some View {
HStack(spacing: 12) {
// 2
TextField("Enter New Task..", text: $taskTitle)
// 3
Button(action: handleSubmit) {
Image(systemName: "plus")
}
}
.padding(20)
}
private func handleSubmit() {
// some action
}
}
下面是对这个视图中添加的每个重要点的解释。
taskTitle与@State属性包装器被用来接收每一个变化的更新。- 然后,我们添加了
TextField视图,使用户能够添加新的文本,并使用$符号将其与taskTitle变量绑定。 - 然后,
handleSubmit被添加到Button视图中,作为动作处理函数,我们将在下一节中与数据插入过程整合。
创建表单后,我们需要更新ContentView 。在ContentView 内添加一个VStack ,并同时包括AddTaskView 和TaskListView 。
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
AddTaskView()
TaskListView()
}
.navigationTitle("Todo")
.navigationBarTitleDisplayMode(.automatic)
}
}
}
如果我们再次运行该项目,输出将显示新添加的表单在列表上方。
创建一个Realm模型
Realm模型是一个普通的Swift类,它子类化了RealmObject 协议,并将创建的对象与Realm数据库模式相一致。Realm对象将被自动保存为Realm数据库中的一个表,并具有所有定义的属性。它还具有额外的功能,如实时查询、反应式事件处理器和实时数据更新。
这些是支持的 Swift 数据类型,可以在 Realm 模型中使用。
StringDataIntDoubleFloatBoolDate
创建TaskObject Realm 模型
首先,我们将创建另一个名为TaskObject 的 Realm 模型。
现在,我们有两个模型,Task 和TaskObject 。RealmTaskObject 只与 Realm 对象协议和数据库通信,而Task 类从 Realmobject 获取数据并与 Swift 视图通信。然后你可以通过Task类对数据进行修改,这样它就可以在应用程序的其他区域使用。Task 模型用于显示将来会有格式化、编码和解码等功能的数据,而TaskObject 是专门为 Realm 数据模型创建的。
创建一个名为TaskObject 的新文件,它继承了 RealmObject 类。请注意,Realm模型中的每个属性都应与@Persisted wrapper一起使用,以标记每个属性为Realm模型的一部分,在读写操作中会被相应处理。
import Foundation
import RealmSwift
class TaskObject: Object {
@Persisted(primaryKey: true) var id: ObjectId
@Persisted var title: String
@Persisted var completed: Bool = false
@Persisted var completedAt: Date = Date()
}
然后,用自定义的init(taskObject:) 函数更新Task 模型,以实现与Realm对象的快速数据映射。
struct Task: Identifiable {
var id: String
var title: String
var completed: Bool = false
var completedAt: Date = Date()
init(taskObject: taskObject) {
self.id = taskObject.id.stringValue
self.title = taskObject.title
self.completed = taskObject.completed
self.completedAt = taskObject.completedAt
}
}
创建任务视图模型
视图模型来实现我们新创建的视图和Realm数据库之间的通信。最初,我们将集中讨论如何插入新的任务和获得所有任务的列表。
创建一个名为TaskViewModel 的新文件,并添加以下代码。
// 1
import Foundation
import Combine
import RealmSwift
// 2
final class TaskViewModel: ObservableObject {
// 3
@Published var tasks: [Task] = []
// 4
private var token: NotificationToken?
init() {
setupObserver()
}
deinit {
token?.invalidate()
}
// 5
private func setupObserver() {
do {
let realm = try Realm()
let results = realm.objects(TaskObject.self)
token = results.observe({ [weak self] changes in
// 6
self?.tasks = results.map(Task.init)
.sorted(by: { $0.updatedAt > $1.updatedAt })
.sorted(by: { !$0.completed && $1.completed })
})
} catch let error {
print(error.localizedDescription)
}
}
// 7
func addTask(title: String) {
let taskObject = TaskObject(value: [
"title": title,
"completed": false
])
do {
let realm = try Realm()
try realm.write {
realm.add(taskObject)
}
} catch let error {
print(error.localizedDescription)
}
}
// 8
func markComplete(id: String, completed: Bool) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
try realm.write {
task?.completed = completed
task?.completedAt = Date()
}
} catch let error {
print(error.localizedDescription)
}
}
}
下面是对上述代码中添加的每个重要点的解释。
- 有两个额外的框架需要被导入,
Combine和RealmSwift。Combine是一个强大的Swift API,可以管理异步事件,是原生iOS框架的一部分,所以我们可以直接把它们导入到我们的项目中,不需要任何安装。RealmSwift也是需要的,以便在访问Realm数据库时使用其功能。 - 视图模型是对
ObservableObject协议的子类化,它将向视图发出重要的变化。 tasks正在使用@Published包装器,以使订阅者的视图在其值更新时能够接收更新。token是一个RealmNotificationToken,持有observer对象。setupObserver()主要是设置一个观察者来观察TaskObject列表上的任何变化,例如添加、更新和删除操作。- 每当
tasks变量上发生变化时,它将通知所有的订阅者视图。结果将先按未完成的任务进行排序,然后是已完成的任务。 - 然后,我们添加了一个名为
addTask()的函数,它允许我们创建新的对象来存储在Realm数据库中。 - 然后,我们添加了另一个函数
markComplete(),通过给定的主键(任务ID)改变TaskObject的完成状态。
更新主列表并添加一个表单
完成模型后,我们需要更新TaskListView 和AddTaskView 。
更新TaskListView
在ForEach 参数中,我们现在将传递tasks 作为从Realm数据库中获取的动态数据。我们不需要写额外的函数来保持数据的更新,因为一旦收到来自视图模型的更新,视图会自动重新加载自己。
struct TaskListView: View {
@EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(viewModel.tasks, id: \.id) { task in
TaskRowView(task: task)
Divider().padding(.leading, 20)
}
.animation(.default)
}
}
}
}
AddTaskView
在这一节中,我们通过调用视图模型addTask ,来完成handleSubmit 的功能。
struct AddTaskView: View {
@State private var taskTitle: String = ""
@EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
HStack(spacing: 12) {
TextField("Enter New Task..", text: $taskTitle)
Button(action: handleSubmit) {
Image(systemName: "plus")
}
}
.padding(20)
}
private func handleSubmit() {
viewModel.addTask(title: taskTitle)
taskTitle = ""
}
}
@EnvironmentObject 包装器
环境对象是SwiftUI中一个强大的功能,它可以自动保持多个视图中单个共享对象的变化。
正如我们在TaskListView 和AddTaskView 视图中看到的那样,我们需要使用@EnvironmentObject 包装器,以便观察TaskViewModel 中可能发生的任何变化。
为了使环境对象可以在视图中使用,我们需要使用environmentObject() 来传递该对象。在这种情况下,我们需要更新TodoRealmSwiftUIApp 中的App 文件。
@main
struct TodoRealmSwiftUIApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(TaskViewModel())
}
}
}
棒极了,现在主列表已经完全与Realm数据库集成。让我们运行该项目,尝试添加一些任务,并将其中一些任务标记为完成或未完成。

任务细节视图
在这一部分,我们将增加一个视图,以显示我们列表中每个任务的细节。我们还将为这个新视图添加编辑和删除功能。
创建一个名为TaskView 的新文件,并用以下代码进行更新。
import SwiftUI
struct TaskView: View {
// 1
@EnvironmentObject private var viewModel: TaskViewModel
// 2
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State private var taskTitle: String = ""
// 3
let task: Task
var body: some View {
VStack(alignment: .leading, spacing: 24) {
// 4
VStack(alignment: .leading, spacing: 4) {
Text("Title")
.foregroundColor(Color.gray)
TextField("Enter title..", text: $taskTitle)
.font(.largeTitle)
Divider()
}
// 5
Button(action: deleteAction) {
HStack {
Image(systemName: "trash.fill")
Text("Delete")
}
.foregroundColor(Color.red)
}
Spacer()
}
.navigationBarTitle("Edit Todo", displayMode: .inline)
.padding(24)
.onAppear(perform: {
taskTitle = task.title
})
// 6
.onDisappear(perform: updateTask)
}
private func updateTask() {
viewModel.updateTitle(id: task.id, newTitle: taskTitle)
}
private func deleteAction() {
viewModel.remove(id: task.id)
presentationMode.wrappedValue.dismiss()
}
}
下面是对上述代码中添加的每个重要点的详细解释。
- 在这段代码中,我们使用了
TaskViewModel作为EnvironmentObject变量,以实现对共享视图模型的访问。 - 然后我们用
presentationMode,以编程方式解散视图。 - 在初始化过程中,
task被添加为依赖模型 T``extField,以使我们能够编辑任务的标题。- 然后,我们添加了一个删除按钮,从Realm数据库中删除任务。
- 最后,一旦用户离开视图,
updateTask被调用以保存数据。
更新视图模型
接下来,用删除和更新功能更新TaskViewModel 。
func remove(id: String) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
if let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId) {
try realm.write {
realm.delete(task)
}
}
} catch let error {
print(error.localizedDescription)
}
}
func updateTitle(id: String, newTitle: String) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
try realm.write {
task?.title = newTitle
}
} catch let error {
print(error.localizedDescription)
}
}
为TaskListView 项目添加导航
最后,用NavigationLink ,更新TaskListView 中的项目视图,这样,每当用户点击该行,就会导航到详细视图。
NavigationLink (destination: TaskView(task: task)) {
TaskRowView(task: task)
}

太好了。我们已经成功实现了所有的CRUD操作。
模式迁移
当我们想以下列方式修改数据库模式时,迁移变得非常重要。
- 添加新的属性或字段
- 改变属性的数据类型
- 重命名属性
- 更新属性的默认值
在下面的例子中,我们要添加一个新的任务字段,叫做Due Date。我们将需要对我们的视图和模型做一些小的更新改动。
在我们的视图和模型中添加到期日字段
在TaskObject 和Task 模型中添加一个名为dueDate 的新字段,其类型是可选的Date 。
TaskObject model
我们将创建一个新的TaskObject模型,和我们上面做的一样。
class TaskObject: Object {
@Persisted(primaryKey: true) var id: ObjectId
@Persisted var title: String
@Persisted var completed: Bool = false
@Persisted var completedAt: Date = Date()
// New property
@Persisted var dueDate: Date? = nil
}
Task model
在下面的更新代码中,我们将添加一个新的属性(dueDate),用于格式化日期的计算变量,并更新init 函数。
struct Task: Identifiable {
var id: String
var title: String
var completed: Bool = false
var completedAt: Date = Date()
// New property
var dueDate: Date? = nil
init(taskObject: TaskObject) {
self.id = taskObject.id.stringValue
self.title = taskObject.title
self.completed = taskObject.completed
self.completedAt = taskObject.completedAt
// Also map the new property
self.dueDate = taskObject.dueDate
}
var formattedDate: String {
if let date = dueDate {
let format = "MMM d, y"
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return "Due at " + dateFormatter.string(from: date)
}
return ""
}
}
更新任务视图模型
然后,更新视图模型,在update() 函数中存储到期日期值。
func update(id: String, newTitle: String, dueDate: Date?) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
try realm.write {
task?.title = newTitle
// Update due date value (Optional value)
task?.dueDate = dueDate
}
} catch let error {
print(error.localizedDescription)
}
}
迁移需要的错误
作为提醒,每次用户添加或更新一个新的属性时,都需要迁移。让我们试着在迁移前运行该项目,看看Xcode日志中的错误输出,这将从异常处理程序中捕捉到。
Migration is required due to the following errors:
- Property 'TaskObject.dueDate' has been added.
设置迁移
默认的模式版本是1 ,所以我们必须在配置中把模式改为2 。
用这个代码添加或更新你的AppDelegate 文件。在configMigration 函数中,我们已经指定了模式版本为2 。
import UIKit
import RealmSwift
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
configMigration()
return true
}
private func configMigration() {
// perform migration if necessary
let config = Realm.Configuration(
schemaVersion: 2,
migrationBlock: { migration, oldSchemaVersion in
// additional process such as rename, combine fields and link to other object
})
Realm.Configuration.defaultConfiguration = config
}
}
还要确保包括AppDelegate 适配器。
import SwiftUI
@main
struct TodoRealmSwiftUIApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
...
}
现在,一切都应该正常工作了。运行该项目,结果将与下面的屏幕截图相似。
项目完成
恭喜你!我们已经完成了一个待办事项的构建。我们已经完成了使用Realm和SwiftUI构建一个待办事项应用程序。
谢谢你的阅读,祝你编码愉快