Realm数据库基础教程

96
Thermod
2015.05.05 02:04* 字数 10750

原文 Realm Tutorial

原文作者 Bill Kastanakis

译者及改编 星夜暮晨(QQ:412027805)

2015年4月29日

2016年3月9日,更新至 Xcode7.2.1、Swift2.1.1及Realm 0.97

目前 Realm 已经有了中文版本的说明文档,并且有SwiftObjective-C两个版本。



学习如何使用 Realm 数据库引擎来轻松地实现 Swift 的数据存储

Realm 是一个跨平台的移动数据库引擎,于 2014 年 7 月发布,准确来说,它是专门为移动应用所设计的数据持久化解决方案之一。

Realm 可以轻松地移植到您的项目当中,并且绝大部分常用的功能(比如说插入、查询等等)都可以用一行简单的代码轻松完成!

Realm 并不是对 Core Data 的简单封装,相反地,Realm 并不是关于 Core Data 的一个封装,也不是基于 SQLite 所构建的。它拥有自己的数据库存储引擎,可以高效且快速地完成数据库的构建操作。

之前我们提到过,由于 Realm 使用的是自己的引擎,因此,Realm 就可以在 iOS 和 Android 平台上共同使用(完全无缝),并且支持 Swift、Objective-C、Java 以及 JavaScript 语言来编写,Android 平台和 iOS 平台使用不同的SDK,如果使用 React Native SDK 的话,就可以使用 JavaScript 同时编写 Android 和 iOS 应用了。

数以万计的使用 Realm 的开发者都会发现,Realm 比 SQLite 以及 Core Data 要快很多。下面我们给出一个例子,分别展示 Core Data 和 Realm 在执行一个断言查询请求并且排序结果所使用的代码量:

// Core Data
let fetchRequest = NSFetchRequest(entityName: "Specimen")
let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString)
fetchRequest.predicate = predicate
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
    let results = try managedObjectContext.executeFetchRequest(fetchRequest)
} catch let error {
    print(error)
}

而换成了 Realm 呢?您会惊叹于 Realm 的简单的:

// Realm Swift
let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString)
let specimens = realm.objects(Specimen).filter(predicate).sorted("name", ascending: true)
// Realm Objective-C
NSPredicate* predicate = [NSPredicate predicateWithFormat: @"name BEGINSWITH [c]%@", searchString];
RLMResults* specimens = [Specimen objectsWithPredicate: predicate];

使用 Realm 可以让代码变得十分简洁,从而让您的代码易读易写。

综上所述,我们之所以使用 Realm 的理由不外乎如下几点:

  • 跨平台:现在绝大多数的应用开发并不仅仅只在 iOS 平台上进行开发,还要兼顾到 Android 平台的开发。为两个平台设计不同的数据库是愚蠢的,而使用 Realm 数据库,iOS 和 Android 无需考虑内部数据的架构,调用 Realm 提供的 API 就可以完成数据的交换,实现“一个数据库,两个平台无缝衔接”。

  • 简单易用:Core Data 和 SQLite 冗余、繁杂的知识和代码足以吓退绝大多数刚入门的开发者,而换用 Realm,则可以极大地减少学习代价和学习时间,让应用及早用上数据存储功能。

  • 可视化:Realm 还提供了一个轻量级的数据库查看工具,借助这个工具,开发者可以查看数据库当中的内容,执行简单的插入和删除数据的操作。毕竟,很多时候,开发者使用数据库的理由是因为要提供一些所谓的“知识库”。

本教程将会向您介绍 Realm 在 iOS 平台上的简单应用,即导入 Realm 框架、创建数据模型、执行查询以及插入、更新和删除记录,以及使用既有的数据库。

提示:原文教程写于 2014 年,而 Realm 的版本更新得十分快,因此,本教程并不会拘泥于原文教程所述内容,而是根据 Realm 的版本更新进行相关修改。
原文作者提到,要在 Realm 抵达1.0版本的时候再来更新这篇教程,大家尽请期待吧!

让我们开始吧

我们将会以一个实际的项目来进行教程:假设您在西双版纳自然保护区觅得了一份职位“监测员”,职责是记录这个“动植物王国”当中所发现物种的相关信息,包括种群数量、发现区域、年龄结构等等。因此,您需要一个助手来帮忙记录您的发现,但是很可惜的是,保护区并没有多余的人手来做您的助手(主要是没钱)。所以没有办法,我们必须为自己制作一个虚拟的“助手”,也就是一个以“我的物种笔记”命名的APP,这样就可以随手记录我们的发现了!

点击此处下载本教程所使用的起始项目

在Xcode当中打开我们的起始项目。此时,MapKit已经在项目当中建立好了,而且项目已经拥有了一些简单的创建、更新和删除物种信息的功能。

提示:如果您对 MapKit 的相关知识感兴趣,可以查看 Introduction to MapKit tutorial,这篇教程将会深入阐述 MapKit 是如何工作的。

现在,让我们前往 Realm 的官网去下载Realm的框架吧!

Realm 的使用需求如下:

  • iOS ≥ 8 或者 Mac OS X ≥ 10.9
  • Xcode ≥ 7.0
  • 现在Realm的版本为:0.98.1

解压下载下来的Realm压缩包。在压缩包中,我们可以看到一个名为ios的文件夹。打开这个文件夹,然后将 swift-2.1.1 文件夹中的两个 framework文件拖入到我们起始项目 General 选项卡的 Embedded Binaries 项目当中。


将框架置入项目当中

之后,一定要确保勾选了Copy Items if needed选项,然后单击Finish按钮就完成了往项目中添加框架的操作。

然后最好将项目中出现的两个 framework 文件拖放到“Frameworks”文件夹中以确保文件有序(强迫症患者~)。

随后,我们打开 Realm 压缩包下的 plugin 文件夹,运行其中的 RealmPlugin.xcodeproj 项目,编译并运行该项目,重启 Xcode。

这么做的目的是为了安装一个 Realm 插件,这个插件能够帮助我们更好更快地创建 Realm 数据模型。

如果您需要为应用撰写单元测试或者 UI Test的话,那么您需要给这个测试目标"Build Settings"中的"Framework Search Paths",添加RealmSwift.framework 的上级目录,在本例中添加 $(PROJECT_DIR) 即可,如图所示:


添加 RealmSwift 路径

如果您需要让应用能够上架的话,那么就需要给应用目标的"Build Phases" 中创建一个新的"Run Script Phase",并在文本框中写入

bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework/strip-frameworks.sh"

因为要绕过这个APP商店提交的bug,这一步在打包二进制发布版本时是必须的。

好的,我们的准备工作就完成了!您可以尝试运行一下起始项目,以确保没有任何错误产生。如果出现错误的话,请仔细查看上面所述的一些步骤,确保没有任何疏漏发生。运行成功后的基本界面如下所示:


应用界面

Realm Browser介绍

Realm资源包中包含了一个很有用的实用工具,可以帮助我们更好地管理Realm数据库,那就是Realm Browser

Realm Browser可以让您轻松地读写Realm数据库(以.realm结尾),因此我们无需头疼如何去查看Realm专有数据库的逻辑结构以及其中的数据,可视化的操作就如同SQLite的其他数据库查看工具一样,十分简单、易用(虽然Realm Browser的功能还十分简陋,真的只能读写而已)。


Realm Browser

Realm Browser可以前往 Mac App Store 进行下载。

您可以尝试在Realm Browser中选择Tools -> Generate demo database来试着探索一下Realm Browser的功能。

Realm相关术语和主要类

为了帮助您更好地理解Realm的使用,下面我们将会对Realm的相关术语和主要类进行一个大致的介绍:

  • Realm:Realm是框架的核心所在,是我们构建数据库的访问点,就如同Core Data的管理对象上下文(managed object context)一样。出于简单起见,realm提供了一个默认的 Realm() 的便利构造器方法,在本教程中我们就仅使用这个便利构造器方法来完成我们所需的功能。当然,我们也可以导入外部已经编写好的realm数据库文件,也可以在我们不需要将数据保存在硬盘上时使用“内存实例对象”(in-memory realm instance),此外,还可以同时使用多个数据库文件。

  • Object:这是我们自定义的realm数据模型。创建数据模型的行为将会影响到数据库的结构。要创建一个数据模型,我们只需要继承Object,然后设计我们想要存储的属性即可。

  • 关系(Relationships):通过简单地在数据模型中声明一个Object类型的属性,我们就可以创建一个“一对多”的对象关系。同样地,借助List我们还可以创建“多对一”和“多对多”的关系。

  • 写操作事务(Write Transactions):数据库中的所有操作,比如创建、编辑,或者删除对象,都必须在事务中完成。“事务”是指位于write闭包内的代码段。

  • 查询(Queries):要在数据库中检索信息,我们需要用到“检索”操作。检索最简单的形式是对Realm()数据库发送objects(_:)消息。如果需要检索更复杂的数据,那么还可以使用断言(predicates)、复合查询以及结果排序等等操作。

  • Results:这个类是执行任何查询请求后所返回的类,其中包含了一系列的Object对象。和Array类似,我们可以用下标语法来对其进行访问,并且还可以决定它们之间的关系。不仅如此,它还拥有许多更强大的功能,包括排序、查找等等操作。

现在您应该对Realm有了一个大概的了解了,现在是时候来试着使用Realm来完成起始项目的剩余工作了。

创建第一个数据模型

好了,前面我们废话了这么多,现在终于要开始使用数据库了。首先我们要创建一个数据模型。

右键选择Xcode项目导航器中的Model组,然后选择New File -> iOS -> Realm -> Realm Model Object,创建一个新的模型文件,将其命名为SpeciesModel并且确保选中了Swift语言。


创建模型

提示:您也可以自行创建数据模型,但是用这个插件的话无疑能让生活更美好,不是么?

打开SpeciesModel.swift文件,然后用以下代码替换文件中的内容:

import UIKit
import RealmSwift

class SpeciesModel: Object {
    dynamic var name = ""
    dynamic var speciesDescription: String?
    dynamic var latitude: Double = 0
    dynamic var longitude: Double = 0
    dynamic var created = NSDate()
}

上面的代码添加了一些属性来存储信息:name属性存储物种名称,speciesDescription存储物种的描述信息。通过给属性添加可选值,可以将该属性声明为可选或者不可选,目前支持可选值的类型有String、NSDate 以及 NSData 类型。因此,物种的描述信息是可选的,我们可以不存储这个信息。

latitude以及longitude存储了物种的经纬度信息。在这里我们将其类型设置为Double(CLLocationDegrees是Double的别名),并且使用0来进行初始化。

最后,created存储了这个物种所创建的时间信息。NSDate()将会返回当前时间,因此我们就用这个值来初始化这个属性

好了,现在我们就成功创建了第一个Realm数据模型了,要不要动动脑来完成一个小小的挑战呢?

我们知道,这些物种将会被划分为不同的“类别”,您的任务就是自行创建一个“类别”数据模型,这个文件将被命名为CategoryModel.swift,然后这个新的数据模型只要一个字符串类型的属性——name

以下是解决方案的代码:

import UIKit
import RealmSwift

class CategoryModel: Object {
    dynamic var name = Categories.Uncategorized.rawValue
}

我们现在拥有了CategoryModel数据模型了,下面我们将通过某种方式将其与SpeciesModel数据模型关联起来,搭建起“关系”。

重新回顾一下上一节的内容,我们可以通过简单地声明一个属性来创建数据模型之间的关系。

打开SpeciesModel.swift文件,然后在created属性下面添加如下语句:

dynamic var category: CategoryModel?

这个语句设置了“物种”和“类别”之间的“多对一”关系,这就意味着每个物种都只能够拥有一个类别,但是一个类别可以从属于多个物种。

好的,我们创建完了一个基础数据模型了,现在是时候向数据库中添加数据了!

添加数据

在此之前,我们需要创建一个公共的 Realm 实例,打开 AppDelegate.swift 文件,在文件顶部导入 RealmSwift 框架,然后加入以下语句

let realm = try? Realm()

这样就创建了一个全局的 realm 实例,在我们的例子中我们将一直使用这个实例。

每当用户添加了一个新的物种标记,用户就可以对这个标记进行修改,比如说设置物种名字,选择类别等等。打开CategoriesTableViewController.swift文件。这个视图控制器将要在这个表视图中显示类别清单,以便用户可以选择。

因此,我们需要在应用初始运行时,给用户提供几个默认的类别以供选择。

在类定义当中添加以下方法,别忘了在文件顶部导入RealmSwift框架(import RealmSwift):

private func populateDefaultCategories() {
    guard let realm = realm else { return }    // 1
    let fetchResults = realm.objects(CategoryModel)  // 2
    self.categories = fetchResults

    if categories.count != 0 { return }  // 3
    try! realm.write {   // 4
        for category in Categories.allValues {   
            let newCategory = CategoryModel()
            newCategory.name = category.rawValue
            realm.add(newCategory)   // 5
        }
    }
}

对应的标号注释如下:

  1. 我们访问默认的realm单例对象,然后将其用realm变量简单表示,以供访问

  2. Realm().objects(_:)这个方法将会返回所有对应对象的Results实例。在本例中,我们向数据库中的CategoryModel对象发送了一个查询请求,返回这个表当中的所有行信息。注意的是,这里我们得到的是一个Results<CategoryModel>对象,这个对象用来存放我们的查询结果。

  3. 如果查询结果中的元素数量为0,那么就说明数据库当中没有类别信息的相关记录,那么就意味着这是用户第一次启动应用。

  4. 这一步将在默认realm数据库中启动一个写操作事务——现在,我们就可以向数据库当中添加记录了。

  5. 对于每个类别名称来说,我们创建了一个对应的CategoryModel实例对象,然后设置其name属性,最后将这个对象添加到realm当中。

您可以像上面我们做的那样,执行一些简单的创建操作,或者您可以执行一些复杂的操作,比如说同时创建、更新、删除多个对象等等。

然后在viewDidLoad()方法的底部加入以下代码:

self.populateDefaultCategories()

这个方法将会在视图加载的过程中,添加我们的测试用类别,并且执行向数据库写入数据的操作。

好了,现在我们的数据库当中已经有了一些数据了,我们需要更新一下表试图数据源相关方法,以显示这些类别。找到tableView(_:cellForRowAtIndexPath:)方法,然后用以下代码替换它:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("CategoryCell", forIndexPath: indexPath) 
    cell.textLabel?.text = categories[indexPath.row].name
    return cell
}

这个声明语句从categories对象当中读取对应行的名称,然后设置到单元格的文本标签上面显示。

接下来,添加一个新的属性:

var selectedCategory: CategoryModel!

我们用这个属性来存储当前选中的类别。

找到tableView(_: willSelectedRowAtIndexPath:),然后用以下代码替换它:

override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
    selectedCategory = self.categories[indexPath.row]
    return indexPath
}

上面声明的方法将会在用户点击某个单元格的时候,将用户点击的类别存储在selectedCategory属性当中。

编译并运行这个应用,然后尝试定位到某个您感兴趣的位置(使用模拟器的位置模拟),然后点击右上角的“+”按钮创建一个新的标记点。点选地图上的这个标记点,然后点击其弹出来的气泡,接下来会弹出这个标记点的详细信息。随后,点击类别文本框,就可以看到如下图所示的类别列表了:


类别列表

您可以选择其中一个类别,不过这个操作仅仅只是将其保存到属性当中。如果您感兴趣,可以前往模拟器的Documents目录下面,使用Realm Browser查看我们生成的数据库,在里面就可以看到我们写入的数据了,这是不是很令人激动呢?

使用Realm Browser

通常情况下,使用defaultRealm()方法生成的数据库文件将会存放在/Users/(Your Account)/Library/Developer/CoreSimulator/Devices/(Simulator ID)/data/Containers/Data/Application/(Application ID)/Documents/路径下面,名为default.realm。Simulator ID指的是您运行的模拟器的ID,Application ID指的是这个应用所分配到的ID。

如果您仍然不清楚这个Realm数据库在哪儿的话,那么使用如下语句,就可以打印处这个数据库所在的完整位置了:

print(realm?.path)

在这个Documents目录下面,我们可能会看到两个文件。一个是default.realm文件,这个是数据库文件,里面就是数据的存放点了。而另一个则是default.realm.lock文件,这个文件也有可能不会存在,它是用来当数据库文件被使用时,防止其它应用对其进行修改的。

双击这个default.realm文件,就可以使用Realm Browser打开了:


Realm Browser打开的default.realm文件

注意:如果default.realm已经在其它应用中打开了,那么强行打开它就可能会出现异常。

.lock文件就可以防止对default.realm文件的重复操作,在使用Realm Browser打开数据库文件前,请先确保应用没有在运行,然后删除.lock文件,才能打开。

一旦数据库在Realm Browser中被打开,您将会看到CategoryModel类中拥有6个对象,这就意味着这个“表”中已经存放了6个记录了。点击这个类就可以查看这个类当中拥有的具体对象信息。

增加类别

好了,现在我们就可以来实现“为某个物种添加类别”的功能了。

打开AddNewEntryController.swift,然后向类中添加以下属性:

var selectedCategory: CategoryModel!

我们将会用这个属性来存储我们在CategoriesTableViewController选中的类别。

接下来,找到unwindFromCategories(segue:)方法,然后在方法底部添加以下代码:

selectedCategory = categoriesController.selectedCategories
categoryTextField.text = selectedCategory.name

这个方法会在用户从categoriesTableViewController中选择了一个类别后被调用。在这里,我们获取到了这个选择的类别,然后将其存储在本地属性selectedCategory当中,接着,我们将它的值填充到文本框里面。

现在,我们已经完成了类别的获取,接下来就是要创建第一个物种了!

仍然还是在AddNewEntryController.swift当中,向类中再添加一个属性:

var species: SpeciesModel!

这个属性将会存储一个新的物种数据模型对象。

接下来,导入 RealmSwift 框架,然后向类中添加以下方法:

private func addNewSpecies() {
    guard let realm = realm else { return }   // 1
    try! realm.write {  // 2
        let newSpecies = SpeciesModel()  // 3
        // 4
        newSpecies.name = self.nameTextField.text!
        newSpecies.category = self.selectedCategory
        newSpecies.speciesDescription = self.descriptionTextView.text
        newSpecies.latitude = self.selectedAnnotation.coordinate.latitude
        newSpecies.longitude = self.selectedAnnotation.coordinate.longitude
        self.species = newSpecies
        realm.add(newSpecies)  // 5
    }
}

对应的标号注释如下:

  1. 获取默认的Realm数据库

  2. 开启一个事务序列,准备写入数据

  3. 创建一个Species对象实例

  4. 接着,设置这个对象的相关值。这些值来自于用户界面的文本输入框。

  5. 向realm中写入新的Species对象

在这里,我们需要使用“输入验证”,来确保用户的输入是正确的。在工程中已经有了一个存在的validateFields()方法来执行输入验证的工作,以确保物种名称和描述不能为空。我们刚刚增加了设置类别的功能,那么我们应该也要确保类别选择不能为空。

validateFields()方法中找到以下代码:

if nameTextField.text.isEmpty || descriptionTextView.text.isEmpty {

将其变更为:

if nameTextField.text.isEmpty || descriptionTextView.text.isEmpty || selectedCategory == nil {

这个方法经能够确保所有的文本框都有值,并且用户也已经选择了一个类别。

接下来,向类中添加以下方法:

override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
    if validateFields() {
        if species == nil { addNewSpecies() }
        return true
    } else {
        return false
    }
}

在上面的代码中,我们调用了输入验证的方法,如果所有文本框都有值的话,那么就可以添加一个新的物种。

编译并运行您的应用,单击“+”按钮来创建一个新的物种。然后输入其名称和描述,选择一个类别,接着单击“保存”按钮来将这个物种添加到数据库中。


添加新的数据

视图消失了——等等,怎么什么都没有发生呢?什么情况?

哦对了,我们确实已经向Realm数据库提交了一个数据,但是我们还没有在地图上做出相应的设置和改变。

检索数据

既然我们已经向数据库中添加了一个物种了,那么现在我们希望它能够在地图上显示出来。

如果您想要检视这个新数据,那么打开Realm Browser就可以查看数据了。记住要先退出模拟器。


添加的物种信息

我们仅仅只能够看见孤零零的一条记录,里面存储了记录的名称、描述信息、经纬度信息、添加的时间。还有最重要的,就是我们看到了连接到CategoryModel的category记录,这就意味着我们已经创建好了物种和类别的“一对多”关系。点击这个蓝色的超链接,我们就可以查看CategoryModel的相关数据了。

好的,回到正题,我们现在需要在地图上显示新添加的数据。

打开SpeciesAnnotation.swift,然后向类中添加一个新的属性:

var species: SpeciesModel?

这个属性将会为这个标记点保存它所拥有的物种信息。

接下来,用以下代码替换构造器:

init(coordinate: CLLocationCoordinate2D, title: String, sub: Categories, species: SpeciesModel? = nil) {
    self.coordinate = coordinate
    self.title = title
    self.subtitle = sub.rawValue
    self.species = species
}

我们所做的改变,就是给这个构造器方法添加了一个带默认值的构造器参数,以便可以对species属性进行赋值。默认值为nil,这意味着我们可以忽略这个参数,使用前面三个参数进行初始化也是没有任何问题的。

打开MapViewController.swift,然后向类中添加一个新属性(同样地,别忘了导入RealmSwift):

private var results: Results<CategoryModel>?

如果我们想要在用属性来存储一系列物种,那么我们需要将这个属性声明为RLMResults类型。要记住,我们是不能够初始化RLMResults对象的,我们必须要通过查询操作来获取它的值。

现在我们需要一些方法来获取所有的物种数据。仍然还是在MapViewController.swift当中,向类中添加如下方法:

private func populateMap() {
    mapView.removeAnnotations(mapView.annotations)   // 1

    guard let realm = realm else { return }
    let results = realm.objects(SpeciesModel)  // 2
    self.results = results

    for result in results {
        let coordinate = CLLocationCoordinate2D(latitude: result.latitude, longitude: result.longitude)
        let category: Categories
        if let cate = result.category, newCate = Categories(rawValue: cate.name) {
            category = newCate
        } else {
            category = .Uncategorized
        }
        let speciesAnnotation = SpeciesAnnotation(coordinate: coordinate, title: result.name, sub: category, species: result)  // 3
        mapView.addAnnotation(speciesAnnotation)  // 4
    }
}

对应的标号注释如下:

  1. 首先,我们先清除了地图上所有存在的标记点,这样我们就不用考虑其他的要素

  2. 然后,我们从Realm数据库中获取Species的全部数据

  3. 我们在此创建了一个自定义的SpeciesAnnotation

  4. 最后,我们往MKMapView上添加这个标记点

好的,现在我们可以在某处地方吊用这个方法了。找到viewDidLoad()然后将这个方法加入到这个方法底部:

populateMap()

这样就确保了每当地图视图控制器加载的时候,地图就能够显示Species标记点。

接着,我们仅需要修改标记点的名称和类别即可。找到unwindFromAddNewEntry(),然后使用下列代码替换掉该方法:

@IBAction func unwindFromAddNewEntry(segue: UIStoryboardSegue) {

    let addNewEntryController = segue.sourceViewController as! AddNewEntryController
    let addedSpecies = addNewEntryController.species
    let addedSpeciesCoordinate = CLLocationCoordinate2D(latitude: addedSpecies.latitude, longitude: addedSpecies.longitude)

    if lastAnnotation != nil {
        mapView.removeAnnotation(lastAnnotation)
    } else {
        for annotation in mapView.annotations where annotation is SpeciesAnnotation {
            let currentAnnotation = annotation as! SpeciesAnnotation
            if currentAnnotation.coordinate == addedSpeciesCoordinate {
                mapView.removeAnnotation(currentAnnotation)
                break
            }
        }
    }

    let category: Categories
    if let cate = addedSpecies.category, newCate = Categories(rawValue: cate.name) {
        category = newCate
    } else {
        category = .Uncategorized
    }
    let annotation = SpeciesAnnotation(coordinate: addedSpeciesCoordinate, title: addedSpecies.name, sub: category, species: addedSpecies)
    mapView.addAnnotation(annotation)

    lastAnnotation = nil
}

这个方法将会在我们从AddNewEntryController返回的时候被调用,然后这时候就会有一个新的物种被添加到地图上方。当我们添加了一个新的物种到地图上,那么就会产生一个标记图标。然后我们想要根据物种的类别来改变其图标的样式,在这个代码里面,我们就是简单的移除了最后添加的这个标记点,然后将其替换为有名称和类别的标记点。

编译并运行您的应用,创建一些不同的物种种类来查看现在地图是什么样式的吧!


添加的标记点效果

另外一个视图

您或许已经注意到在地图视图的左上角有一个“编辑”的按钮。为了更好地管理地图上的记录点,我们这个应用设置了一个基于文本的表视图,用来列出地图上所有的记录点,这个视图我们现在命名为“记录”视图。现在,这个表视图仍然还是空的,现在我们就来向里面填充数据吧!

打开LogViewController.swift,然后将species属性替换成以下形式(同样地,要导入RealmSwift):

private var species: Results<SpeciesModel>?

在上面的代码中,我们用RLMResults替换掉了之前的一个空数组占位符,这个操作和我们在MapViewController所做的一样。

接下来,找到viewDidLoad()方法,然后在super.viewDidLoad()语句下添加以下代码:

self.species = realm.objects(SpeciesModel).sorted("name", ascending: true)

这行代码会将数据库中的所有物种全部输出到species当中,并且按照名字进行排列。

接下来,用以下代码替换tableView(_:cellForRowAtIndexPath:)

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! LogCell
    let speciesModel = self.species[indexPath.row]
    cell.titleLabel.text = speciesModel.name
    cell.subtitleLabel.text = speciesModel.category?.name
    cell.iconImageView.image = Categories(rawValue: speciesModel.category!.name)!.annotationImage
    return cell
}

这个方法将会展示物种的名字和物种的类别,以及其图标。

编译并运行应用,单击左上角的“编辑”按钮,然后您就会在表视图中看到我们之前录入的物种信息,如图所示:


记录界面

删除记录

现在我们已经学习了如何在Realm中创建记录数据,但是如果我们不小心添加了错误的标记点,或者想要移除之前添加过的物种数据,那么我们应该要怎么做呢?因此,我们就需要添加从Realm中删除数据的功能。您会发现这是一个非常简单的操作。

打开LogViewController.swift文件,然后添加以下方法:

func deleteRowAtIndexPath(indexPath: NSIndexPath) {
    let objectToDelete = species[indexPath.row]  // 1
    try! realm.write {  // 2
        realm.delete(objectToDelete) // 3 
        self.species = realm.objects(SpeciesModel).sorted("name", ascending: true)  // 4
        self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)  // 5
    }
}

对应的标号注释如下:

  1. 我们从数据中找到我们想要删除的对象

  2. 启动写操作事务

  3. 调用delete()方法,将要删除的对象传递进去,realm会自动帮我们执行删除操作

  4. 一旦我们移除了一个物种,我们需要重新读取数据

  5. 最后,我们更新UITableViewCell,将单元格移除

接着,找到tableView(_:commitEditingStyle: forRowAtIndexPath:)方法,然后将以下代码加入到if语句块当中:

deleteRowAtIndexPath(indexPath)

当表视图执行一个单例删除操作时,会调用这个协议代理,我们所需要做的就是调用我们刚刚创建的那个方法。

编译并运行您的应用,查看“记录”界面,然后在某个记录上面左滑删除。随后关闭模拟器,用Realm Browser打开数据库,我们就可以看到我们成功执行了更改:


执行删除操作

使用断言

我们仍然还想要给这个应用提供一些碉堡的功能,那么快速查找怎么样?在海量的数据中进行查找还是很麻烦的一件事情,但是有了快速查找,一切就都简单了。我们现在所拥有的这个项目已经包含了一个UISearchController控件,您所需要做的就是添加一点小小的修改,让这个功能能够在Realm中正常工作。

打开LogViewController.swift,然后将searchResults属性替换为以下代码:

private var searchResults: Results<SpeciesModel>!

因为我们仍然是执行“检索”操作,因此我们的数据是存放在Results<SpeciesModel>当中的。

向类中添加以下方法:

func filterResultsWithSearchString(searchString: String) {
    let results = realm.objects(SpeciesModel).filter("name BEGINSWITH [c]'\(searchString)'")   // 1
    let scopeIndex = searchController.searchBar.selectedScopeButtonIndex
    switch scopeIndex {
    case 0:
        searchResults = results.sorted("name", ascending: true)  // 2
    case 1:
        searchResults = results.sorted("distance", ascending: true)   // 3
    case 2:
        searchResults = results.sorted("created", ascending: true)   // 4
    default:
        searchResults = results
    }
    tableView.reloadData()
}

对应的标号注释如下:

  1. 在这里我们调用 Results().filter(_:...) 方法来实现查询,我们创建了一个字符串版本的“断言(predicate)”,在这里,我们搜索以searchString开头的name属性。[c]可以让BEGINSWITH以不区分大小写的灵敏度来进行查找,要注意,searchString是被单引号括起来的。

  2. 如果选中的标签是“名字”,那么结果就按照“名字A-Z”排列

  3. 如果选中的标签是“距离”,那么就按照距离排列结果

  4. 如果选中的标签是“创建时间”,那么就按照时间来进行排列。

因为搜索会导致表视图调用同样的数据源方法,因此我们需要对tableView(_:cellForRowAtIndexPath:)进行小小的修改,以便让其能够处理主要的表视图记录以及查询结果。在这个方法里面,找到以下代码:

let speciesModel = self.species[indexPath.row]

将其替换为以下代码:

let speciesModel: SpeciesModel
if searchController.active {
    speciesModel = self.searchResults[indexPath.row]
} else {
    speciesModel = self.species[indexPath.row]
}

上面这行代码将会检查searchController是否激活。如果激活的话,那么就接收并显示搜索结果的数据;如果不是的话,那么就接收并显示species全部数据。

最后,我们需要一个功能,那就是单击范围栏上的按钮时,更变返回结果的排列顺序。

将空scopeChanged方法用以下代码来替换:

@IBAction func scopeChanged(sender: UISegmentedControl) {
    let species = realm.objects(SpeciesModel)
    switch sender.selectedSegmentIndex {
    case 0:
        self.species = species.sorted("name", ascending: true)
    case 1:
        break
    case 2:
        self.species = species.sorted("created", ascending: true)
    default:
        self.species = species
    }
    tableView.reloadData()
}

在上面的代码中,我们将会检查范围栏上的按钮是哪一个被按下(A-Z,距离,以及添加日期),然后调用sorted()来进行排序。通常情况下,这个列表将按照名字来排序。

您可能会注意到,现在按照距离排序这一块中目前还是空的(case 1),那是因为目前数据模型中还不存在“距离”这么一个玩意儿,因此我们暂时还不需要做这个工作,等到以后添加了再来完善。不过现在,我们已经可以看到它的大致功能了!

编译并运行您的应用,尝试一些搜索操作,然后查看结果!


查看不同的结果

提示:在作者的原教程中,搜索功能实际上是无法实现的。如果您在“合适”的地方添加了相关方法,那么实际上程序仍然还是无法执行搜索功能的。它会提示cell的titleLabel的值为nil。因为在原教程中,Cell是在Storyboard里面自定义的,而搜索栏则是要显示一个新的表视图。如果需要重用自定义的Cell,那么最好需要在Xib文件中进行制作。因为如果没有init(style:reuseIdentifier:)方法的Cell自定义类,是无法进行重用的。

更新记录

我们现在已经实现了添加和删除记录的功能了,剩下就是更新数据功能了。

如果您试着单击LogViewController中的一个单元格,那么就会跳转到AddNewEntryViewController页面,但是这些区域都是空白的。当然,我们首先要做的就是让这个页面显示数据库中存放的数据,以便让用户编辑。

打开AddNewEntryViewController.swift文件,然后向类中添加以下方法:

private func fillTextfields() {
    if species == nil { return }
    nameTextField.text = species.name
    categoryTextField.text = species.category?.name
    descriptionTextView.text = species.speciesDescription
}

这个方法将会使用species中的数据来填充用户界面的文本框。记住,AddNewEntryViewController只有在添加新物种时才会保持文本框为空的状态。

接下来,向viewDidLoad()方法的末尾添加以下语句:

if let species = self.species {
    title = "编辑\(species.name)"
    fillTextfields()
} else {
    title = "添加新的物种"
}

上面这些代码段设置了导航栏的标题,以通知用户当前其是在添加新的物种还是在更新一个已存在的物种信息。如果species不为空,那么就调用fillTextFields方法来填充文本框。

现在我们需要一个更新功能,以便响应用户的更改操作。向类中添加以下方法:

private func updateSpecies() {
    try! realm.write {
        self.species.name = nameTextField.text!
        self.species.category = selectedCategory
        self.species.speciesDescription = descriptionTextView.text
    }
}

在这个事务中,我们只是简单的更新了这三个数据域的值。

这四行短短的代码就足以完成Species记录的更新操作了哦~O(∩_∩)O~

现在我们只需要在用户单击保存按钮的时候调用上述代码即可。找到shouldPerformSegueWithIdentifier(_:sender:),然后在return true语句之前,第一个if代码块之内添加以下代码:

else {
    updateSpecies()
}

当恰当的时候,就会调用这个方法来对数据进行更新。

现在打开LogViewController.swift,然后将prepareForSegue(_:sender:)用以下代码替换:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "Edit" {
        let controller = segue.destinationViewController as! AddNewEntryController
        var selectedSpecies: SpeciesModel!
        guard let indexPath = tableView.indexPathForSelectedRow else { return }

        if let searchResultsController = searchController.searchResultsController as? UITableViewController where searchController.active {
            if let indexPathSearch = searchResultsController.tableView.indexPathForSelectedRow {
                selectedSpecies = searchResults[indexPathSearch.row]
            }
        } else {
            selectedSpecies = species[indexPath.row]
        }
        controller.species = selectedSpecies
    }
}

我们在这里将选中的物种信息传递给了AddNewEntryController。上面的if/else代码是因为要根据用户是否是在查看搜索结果来决定的。

编译并运行您的应用,打开记录视图,并且选中一个存在的物种。您应该可以看到文本框中已经填充了数据。


更新数据

把剩下的东西结束掉

要让我们的应用变得更加完美,我们就需要实现剩下 的功能。

还记不记得我们没有办法根据距离来排序记录?我们需要在里面添加不少的代码,才能够正常的运行这个功能,但是这个结果是非常值得的。

打开SpeciesModel.swift文件,然后向类中添加一个新的属性。

dynamic var distance: Double = 0

这个属性为保存用户位置和该记录点的距离信息。然而,没有必要去存储distance信息,因为用户位置会随时发生改变。我们想让距离成为这个模型的一部分,但是我们并不想Realm来存储这个数据。

Realm支持一种被称为忽视属性(ignored properties)的东西,然后向类中添加以下代码:

override static func ignoredProperties() -> [String] {
    return ["distance"]
}

要实现忽视属性,只需要重载一个命名为ignoredProperties()的静态方法,然后返回一个字符串数组,里面保存有您不想进行存储的属性。

由于我们并不会存储距离这个属性,很明显地我们需要自己计算距离。

打开MapViewController.swift,添加以下方法:

private func updateLocationDistance() {
        guard let results = results else { return }
        for result in results {
            let currentLocation = CLLocation(latitude: result.latitude, longitude: result.longitude)
            let distance = currentLocation.distanceFromLocation(mapView.userLocation.location!)
            try! realm.write {
                result.distance = distance
            }
        }
    }

对于每个物种,我们计算了这个标记点与用户当前位置之间的距离。即时我们没有存储这个距离信息,我们仍然需要将其存储在记录当中,然后将其在写操作事务中保存这个变化消息。

接下来,在prepareForSegue(_:sender:)方法底部添加以下代码:

else if segue.identifier == "Log" {
    updateLocationDistance()
    lastAnnotation = nil
}

现在,在用户打开“记录界面”之前,我们需要调用这个方法来计算距离。

接下来,打开LogViewController.swift,然后找到tableView(_:cellForRowAtIndexPath:)方法。然后在这个方法底部附近,return语句之前添加以下代码:

if speciesModel.distance < 0 {
   cell.distanceLabel.text = "N/A"
} else {
   cell.distanceLabel.text = String(format: "%.2f km", speciesModel.distance / 1000)
}

最后,找到scopeChanged()然后将case 1中的break替换成以下代码:

self.species = realm.objects(SpeciesModel).sorted("distance", ascending: true)

编译并运行应用,然后……呃?怎么崩溃掉了?

'RLMException`, reason: 'Column count does not match interface - migration required'

什么鬼?

当我们向Species模型中添加了一个新的distance属性的时候,我们就对架构(schema)进行了变更,但是我们并没有告诉Realm如何处理这个新增的数据段。从旧版本的数据库迁移(migrate)到新版本的数据库的操作超出了本教程的范围。这并不是Realm独有的问题,Core Data同样也需要在添加、变更或者删除新的数据段的时候进行迁移操作。

本教程的简单解决方案就是将模拟器的应用移除掉即可,然后重新编译并运行应用程序。这将会让应用创建一个全新的数据库,使用新的架构。

从模拟器删除这个应用,接下来编译和运行这个应用。然后添加新的物种,接着打开这个记录视图,这时候我们就可以看到如下所示的距离信息:


距离信息

您或许需要模拟一个位置以便能够计算当前距离,在模拟器菜单栏上,选择Debug\Location,然后选择列表中的一个位置模拟。

接下来该何去何从?

您可以点击此处下载完整的项目

在本教程中,我们学习了如何创建、更新、删除以及查找Realm数据库中的数据记录,以及如何使用断言来进行查找,还有按属性名对结果进行排序的方法。

您可能要问了:“看起来Realm似乎是一个新项目,我感觉在一个完备的应用中使用它可能并不稳定”。

Realm最近才想公众开放,但是早在2012年它就已经在公司级别的产品中使用了。我个人已经在我的既有项目中使用Realm了,而且似乎运转起来相当不错。

如果您使用Objective-C,那么Realm是十分稳定的。对于Swift来说,由于Swift版本并不稳定,因此在使用Realm可能会遭遇到版本更迭所引发的一系列语法问题。不过随着Swift的更新,相信Swift的版本改动会越来越少,Realm在Swift上也会越来越稳定。

对于Realm来说,它还有许多在本教程没有介绍到的特点:

  • 迁移(Migrations):在本教程中,我们看到了对Realm架构的修改导致了错误的产生。要学习关于如何在多版本之间迁移数据库的只是,请查看Realm说明文档的“迁移(migrations)”部分。
  • 其他类型的Realm数据库:在本教程中,我们一直都是使用着“默认”Realm数据库,但是我们仍然还可以使用其他类型的Realm数据库,比如说不存储数据的“内存数据库(in-memory realm)”。我们也可以使用多个Realm数据库,如果我们享用多个数据库来保存多种类型的数据的话。

关于Realm的更多信息,您可以查看官方文档,我发现这个文档真的写得十分详尽。

如果您对本教程有什么建议和意见,请到评论区进行讨论,我会尽快处理这些建议和意见的~

 技术文档翻译