学习如何使用 Realm 数据库引擎来轻松地实现 Swift 的数据存储
Realm 是一个跨平台的移动数据库引擎,于 2014 年 7 月发布,准确来说,它是专门为移动应用所设计的数据持久化解决方案之一。
Realm 可以轻松地移植到您的项目当中,并且绝大部分常用的功能(比如说插入、查询等等)都可以用一行简单的代码轻松完成!
Realm 并不是对 Core Data 的简单封装,相反地, Realm 并不是基于 Core Data ,也不是基于 SQLite 所构建的。它拥有自己的数据库存储引擎,可以高效且快速地完成数据库的构建操作。
之前我们提到过,由于 Realm 使用的是自己的引擎,因此, Realm 就可以在 iOS 和 Android 平台上共同使用(完全无缝),并且支持 Swift 、 Objective-C 以及 Java 语言来编写( Android 平台和 iOS 平台使用不同的 SDK )。
数以万计的使用 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] let error = NSError() let results = managedObjectContext?.executeFetchRequest(fetchRequest, error:&error)而换成了 Realm 呢?您会惊叹于 Realm 的简单的:
// Realm let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString); let specimens = Specimen.objectsWithPredicate(predicate).arraySortedByProperty("name", ascending: true)
使用 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 的框架吧: http://static.realm.io/downloads/cocoa/latest
Realm 的使用需求如下:
iOS ≥ 7 或者 Mac OS X ≥ 10.9
Xcode ≥ 6
现在 Realm 的版本为: 0.91.5
解压下载下来的 Realm 压缩包。在压缩包中,我们可以看到一个名为 iOS 的文件夹。打开这个文件夹,然后将 Realm.framework 文件拖入到我们的起始项目中,最好拖放到 “Frameworks” 文件夹中以确保文件有序(强迫症患者 ~ )。
将框架文件拖入到项目当中
之后,一定要确保勾选了 Copy Items if needed 选项,然后单击 Finish 按钮就完成了往项目中添加框架的操作。
之后,定位到项目设置中 SISpeciesNotes 的 General 选项卡,然后在 Link Binary with Libraries 栏目中添加 libc++.dylib 动态库文件。
然后回到解压的 Realm 文件夹中,打开名为 Swift 的文件夹,然后将里面的 RLMSupport.swift 文件拖入到项目当中。这个文件包含了用于 Realm 相关类的 Swift 简便方法,比如说 RLMResults 中的 Generator 方法,这样就可以像使用原生数组一样使用 Realm 数组了。
好的,我们的准备工作就完成了!您可以尝试运行一下起始项目,以确保没有任何错误产生。如果出现错误的话,请仔细查看上面所述的一些步骤,确保没有任何疏漏发生。运行成功后的基本界面如下所示:
应用界面
Realm Browser 介绍
Realm 资源包中包含了一个很有用的实用工具,可以帮助我们更好地管理 Realm 数据库,那就是 Realm Browser 。
Realm Browser 可以让您轻松地读写 Realm 数据库(以 .realm 结尾),因此我们无需头疼如何去查看 Realm 专有数据库的逻辑结构以及其中的数据,可视化的操作就如同 SQLite 的其他数据库查看工具一样,十分简单、易用(虽然 Realm Browser 的功能还十分简陋,真的只能读写而已)。
Realm Browser
Realm Browser 可以在解压的 Realm 文件夹中的 browser 文件夹中找到。您也可以访问 Realm GitHub repository 然后在其中的 tools/RealmBrowser 目录中找到它。
您可以尝试在 Realm Browser 中选择 Tools -> Generate demo database 来试着探索一下 Realm Browser 的功能。
Realm 相关术语和主要类
为了帮助您更好地理解 Realm 的使用,下面我们将会对 Realm 的相关术语和主要类进行一个大致的介绍:
RLMRealm : RLMRealm 是框架的核心所在,是我们构建数据库的访问点,就如同 Core Data 的管理对象上下文( managed object context )一样。出于简单起见, realm 提供了一个名为 defaultRealm 的单例,在本教程中我们就仅使用这个单例来完成我们所需的功能。当然,我们也可以导入外部已经编写好的 realm 数据库文件,也可以在我们不需要将数据保存在硬盘上时使用 “ 内存实例对象 ” ( in-memory realm instance ),此外,还可以同时使用多个数据库文件。
RLMObject :这是我们自定义的 realm 数据模型。创建数据模型的行为将会影响到数据库的结构。要创建一个数据模型,我们只需要继承 RLMObject ,然后设计我们想要存储的属性即可。
关系 (Relationships) :通过简单地在数据模型中声明一个 RLMObject 类型的属性,我们就可以创建一个 “ 一对多 ” 的对象关系。同样地,借助 RLMArray 我们还可以创建 “ 多对一 ” 和 “ 多对多 ” 的关系。
写操作事务 (Write Transactions) :数据库中的所有操作,比如创建、编辑,或者删除对象,都必须在事务中完成。 “ 事务 ” 是指位于 beginWriteTransaction() 以及 commitWriteTransaction() 操作之间的代码段。
查询 (Queries) :要在数据库中检索信息,我们需要用到 “ 检索 ” 操作。检索最简单的形式是对 RLMObject 对象发送 allObjects() 消息。如果需要检索更复杂的数据,那么还可以使用断言( predicates )、复合查询以及结果排序等等操作。
RLMResults :这个类是执行任何查询请求后所返回的类,其中包含了一系列的 RLMObjects 对象。和 NSArray 类似,我们可以用下标语法来对其进行访问,并且还可以决定它们之间的关系。不仅如此,它还拥有许多更强大的功能,包括排序、查找等等操作。
现在您应该对 Realm 有了一个大概的了解了,现在是时候来试着使用 Realm 来完成起始项目的剩余工作了。
创建第一个数据模型
好了,前面我们废话了这么多,现在终于要开始使用数据库了。首先我们要创建一个数据模型,也相当于创建数据库的一个 “ 表 ” 。
右键选择 Xcode 项目导航器中的 Model 组,然后选择 New File -> iOS -> Source -> Swift File ,创建一个新的 swift 文件,将其命名为 SpeciesModel 并且确保选中了 SISpeciesNotes 对象。
提示:您也许查看过 Realm 的开发文档,它里面介绍说可以使用 “ 插件 ” 来完成数据模型的简单创建(也就是新建文件时,可以像新建 Core Data 数据模型文件一样创建一个既定的模板数据模型),但是很遗憾的是,现在这个功能还只支持创建 OC 版本的数据模型文件,我们为了代码的 “ 干净 ” ,就不采用这种方法。
打开 SpeciesModel.swift 文件,然后用以下代码替换文件中的内容:
import UIKit import Realm class SpeciesModel: RLMObject { dynamic var name = "" dynamic var speciesDescription = "" dynamic var latitude: Double = 0 dynamic var longitude: Double = 0 dynamic var created = NSDate() }
上面的代码添加了一些属性来存储信息: name 属性存储物种名称, speciesDescription 存储物种的描述信息。对于 Realm 中的一些特定的数据类型,比如说字符串,必须要初始化。在本例中,我们使用空字符串来进行初始化。
latitude 以及 longitude 存储了物种的经纬度信息。在这里我们将其类型设置为 Double ( CLLocationDegrees 是 Double 的别名),并且使用 0 来进行初始化。
最后, created 存储了这个物种所创建的时间信息。 NSDate() 将会返回当前时间,因此我们就用这个值来初始化这个属性
好了,现在我们就成功创建了第一个 Realm 数据模型了,要不要动动脑来完成一个小小的挑战呢?
我们知道,这些物种将会被划分为不同的 “ 类别 ” ,您的任务就是自行创建一个 “ 类别 ” 数据模型,这个文件将被命名为 CategoryModel.swift ,然后这个新的数据模型只要一个字符串类型的属性 ——name 。
以下是解决方案的代码:
import UIKit import Realm class CategoryModel: RLMObject { dynamic var name = "" }
我们现在拥有了 CategoryModel 数据模型了,下面我们将通过某种方式将其与 SpeciesModel 数据模型关联起来,搭建起 “ 关系 ” 。
重新回顾一下上一节的内容,我们可以通过简单地声明一个属性来创建数据模型之间的关系。
打开 SpeciesModel.swift 文件,然后在 created 属性下面添加如下语句:
dynamic var category = CategoryModel()
这个语句设置了 “ 物种 ” 和 “ 类别 ” 之间的 “ 一对多 ” 关系,这就意味着每个物种都只能够拥有一个类别,但是一个类别可以从属于多个物种。
好的,我们创建完了一个基础数据模型了,现在是时候向数据库中添加数据了!
添加数据
每当用户添加了一个新的物种标记,用户就可以对这个标记进行修改,比如说设置物种名字,选择类别等等。打开CategoriesTableViewController.swift 文件。这个视图控制器将要在这个表视图中显示类别清单,以便用户可以选择。
因此,我们需要在应用初始运行时,给用户提供几个默认的类别以供选择。
在类定义当中添加以下方法,别忘了在文件顶部导入 Realm 框架( import Realm ):
private func populateDefaultCategories() { self.results = CategoryModel.allObjects() // 1 if results.count == 0 { // 2 let realm = RLMRealm.defaultRealm() // 3 realm.beginWriteTransaction() // 4 let defaultCategories = Categories.allValues // 5 for category in defaultCategories { // 6 let newCategory = CategoryModel() newCategory.name = category realm.addObject(newCategory) } realm.commitWriteTransaction() // 7 self.results = CategoryModel.allObjects() } }
对应的标号注释如下:
allobjects() 方法将会返回指定对象的所有元素,在本例中,我们向数据库中的 CategoryModel 对象发送了一个查询请求,返回这个表当中的所有行信息。注意的是,这里我们得到的是一个 RLMResults 对象,这个对象用来存放我们的查询结果。
如果查询结果中的元素数量为 0 ,那么就说明数据库当中没有类别信息的相关记录,那么就意味着这是用户第一次启动应用。
我们访问默认的 realm 单例对象,然后将其用 realm 变量简单表示,以供访问
这一步将在默认 realm 数据库中启动一个事务 —— 现在,我们就可以向数据库当中添加记录了。
这里我们使用已经定义过的 Categories 枚举来创建一个含有全部默认类别的数组。
对于每个类别名称来说,我们创建了一个对应的 CategoryModel 实例对象,然后设置其 name 属性,最后将这个对象添加到 realm 当中。
当我们添加完所有的类别之后,调用 commitWriteTransaction() 方法来关闭事务,并且向数据库提交数据。
只有调用了 commitWriteTransaction() 方法,我们之前做的所有关于事务的操作才能够被成功运行,因为这涉及到 Realm 的内部处理的问题了。您可以像上面我们做的那样,执行一些简单的创建操作,或者您可以执行一些复杂的操作,比如说同时创建、更新、删除多个对象等等。
然后在 viewDidLoad() 方法的底部加入以下代码:
populateDefaultCategories()
这个方法将会在视图加载的过程中,添加我们的测试用类别,并且执行向数据库写入数据的操作。
好了,现在我们的数据库当中已经有了一些数据了,我们需要更新一下表试图数据源相关方法,以显示这些类别。找到 tableView(_:cellForRowAtIndexPath:) 方法,然后用以下代码替换它:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("CategoryCell", forIndexPath: indexPath) as! UITableViewCell cell.textLabel?.text = (results[UInt(indexPath.row)] as! CategoryModel).name return cell }
这个声明语句从 results 对象当中读取对应行的名称,然后设置到单元格的文本标签上面显示。
接下来,添加一个新的属性:
var selectedCategory: CategoryModel!
我们用这个属性来存储当前选中的类别。
找到 tableView(_: willSelectedRowAtIndexPath:) ,然后用以下代码替换它:
override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? { selectedCategories = self.results[UInt(indexPath.row)] as! CategoryModel 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 数据库在哪儿的话,那么使用如下语句,就可以打印处这个数据库所在的完整位置了:
println(RLMRealm.defaultRealm().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!
这个属性将会存储一个新的物种数据模型对象。
接下来,导入 Realm 框架,然后向类中添加以下方法:
func addNewSpecies() { let realm = RLMRealm.defaultRealm() // 1 realm.beginWriteTransaction() // 2 let newSpecies = SpeciesModel() // 3 // 4 newSpecies.name = nameTextField.text newSpecies.category = selectedCategory newSpecies.speciesDescription = descriptionTextView.text newSpecies.latitude = selectedAnnotation.coordinate.latitude newSpecies.longitude = selectedAnnotation.coordinate.longitude realm.addObject(newSpecies) // 5 realm.commitWriteTransaction() // 6 self.species = newSpecies }
对应的标号注释如下:
获取默认的 Realm 数据库
开启一个事务序列,准备写入数据
创建一个 Species 对象实例
接着,设置这个对象的相关值。这些值来自于用户界面的文本输入框。
向 realm 中写入新的 Species 对象
最后,使用 commitWriteTransaction() 提交写操作事务
在这里,我们需要使用 “ 输入验证 ” ,来确保用户的输入是正确的。在工程中已经有了一个存在的 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 ,然后向类中添加一个新属性(同样地,别忘了导入 Realm ):
var results: RLMResults?
如果我们想要在用属性来存储一系列物种,那么我们需要将这个属性声明为 RLMResults 类型。要记住,我们是不能够初始化 RLMResults 对象的,我们必须要通过查询操作来获取它的值。
现在我们需要一些方法来获取所有的物种数据。仍然还是在 MapViewController.swift 当中,向类中添加如下方法:
func populateMap() { mapView.removeAnnotations(mapView.annotations) // 1 if let results = SpeciesModel.allObjects() { // 2 self.results = results for result in results { let species = result as! SpeciesModel let coordinate = CLLocationCoordinate2DMake(species.latitude, species.longitude) let speciesAnnotation = SpeciesAnnotation(coordinate: coordinate, title: species.name, sub: Categories(rawValue: species.category.name)!, species: species) // 3 mapView.addAnnotation(speciesAnnotation) // 4 } } }
对应的标号注释如下:
首先,我们先清除了地图上所有存在的标记点,这样我们就不用考虑其他的要素
然后,我们从 Realm 数据库中获取 Species 的全部数据
我们在此创建了一个自定义的 SpeciesAnnotation
最后,我们往 MKMapView 上添加这个标记点
好的,现在我们可以在某处地方吊用这个方法了。找到 viewDidLoad() 然后将这个方法加入到这个方法底部:
populateMap()
这样就确保了每当地图视图控制器加载的时候,地图就能够显示 Species 标记点。
接着,我们仅需要修改标记点的名称和类别即可。找到 unwindFromAddNewEntry() ,然后使用下列代码替换掉该方法:
@IBAction func unwindFromAddNewEntry(segue: UIStoryboardSegue) { let addNewEntryController = segue.sourceViewController as! AddNewEntryController let addedSpecies = addNewEntryController.species let addedSpeciesCoordinate = CLLocationCoordinate2DMake(addedSpecies.latitude, addedSpecies.longitude) if lastAnnotation != nil { mapView.removeAnnotation(lastAnnotation) } else { for annotation in mapView.annotations { let currentAnnotation = annotation as! SpeciesAnnotation if currentAnnotation.coordinate.latitude == addedSpeciesCoordinate.latitude && currentAnnotation.coordinate.longitude == addedSpeciesCoordinate.longitude { mapView.removeAnnotation(currentAnnotation) break } } } let annotation = SpeciesAnnotation(coordinate: addedSpeciesCoordinate, title: addedSpecies.name, sub: Categories(rawValue: addedSpecies.category.name)!, species: addedSpecies) mapView.addAnnotation(annotation) lastAnnotation = nil }
这个方法将会在我们从 AddNewEntryController 返回的时候被调用,然后这时候就会有一个新的物种被添加到地图上方。当我们添加了一个新的物种到地图上,那么就会产生一个标记图标。然后我们想要根据物种的类别来改变其图标的样式,在这个代码里面,我们就是简单的移除了最后添加的这个标记点,然后将其替换为有名称和类别的标记点。
编译并运行您的应用,创建一些不同的物种种类来查看现在地图是什么样式的吧!
添加的标记点效果
另外一个视图
您或许已经注意到在地图视图的左上角有一个 “ 编辑 ” 的按钮。为了更好地管理地图上的记录点,我们这个应用设置了一个基于文本的表视图,用来列出地图上所有的记录点,这个视图我们现在命名为 “ 记录 ” 视图。现在,这个表视图仍然还是空的,现在我们就来向里面填充数据吧!
打开 LogViewController.swift ,然后将 species 属性替换成以下形式(同样地,要导入 Realm ):
var species: RLMResults!
在上面的代码中,我们用 RLMResults 替换掉了之前的一个空数组占位符,这个操作和我们在 MapViewController 所做的一样。
接下来,找到 viewDidLoad() 方法,然后在 super.viewDidLoad() 语句下添加以下代码:
species = SpeciesModel.allObjects().sortedResultsUsingProperty("name", ascending: true)
这行代码会将数据库中的所有物种全部输出到 species 当中,并且按照名字进行排列。
接下来,用以下代码替换 tableView(_:cellForRowAtIndexPath:) :
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! LogCell var speciesModel: SpeciesModel! speciesModel = species[UInt(indexPath.row)] as! SpeciesModel cell.titleLabel.text = speciesModel.name cell.subtitleLabel.text = speciesModel.category.name cell.iconImageView.image = getImageOfSpecies(speciesModel.category.name) return cell }
这个方法将会展示物种的名字和物种的类别,以及其图标。
编译并运行应用,单击左上角的 “ 编辑 ” 按钮,然后您就会在表视图中看到我们之前录入的物种信息,如图所示:
记录界面
删除记录
现在我们已经学习了如何在 Realm 中创建记录数据,但是如果我们不小心添加了错误的标记点,或者想要移除之前添加过的物种数据,那么我们应该要怎么做呢?因此,我们就需要添加从 Realm 中删除数据的功能。您会发现这是一个非常简单的操作。
打开 LogViewController.swift 文件,然后添加以下方法:
func deleteRowAtIndexPath(indexPath: NSIndexPath) { let realm = RLMRealm.defaultRealm() // 1 let objectToDelete = species[UInt(indexPath.row)] as! SpeciesModel // 2 realm.beginWriteTransaction() // 3 realm.deleteObject(objectToDelete) // 4 realm.commitWriteTransaction() // 5 species = SpeciesModel.allObjects().sortedResultsUsingProperty("name", ascending: true) // 6 tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) // 7 }
对应的标号注释如下:
首先,我们获取到默认的 Realm 数据库
然后,我们从数据中找到我们想要删除的对象
启动写操作事务
调用 deleteObject() 方法,将要删除的对象传递进去, realm 会自动帮我们执行删除操作
接着提交写操作事务,将删除操作提交到数据库当中
一旦我们移除了一个物种,我们需要重新读取数据
最后,我们更新 UITableViewCell ,将单元格移除
接着,找到 tableView(_:commitEditingStyle: forRowAtIndexPath:) 方法,然后将以下代码加入到 if 语句块当中:
deleteRowAtIndexPath(indexPath)
当表视图执行一个单例删除操作时,会调用这个协议代理,我们所需要做的就是调用我们刚刚创建的那个方法。
编译并运行您的应用,查看 “ 记录 ” 界面,然后在某个记录上面左滑删除。随后关闭模拟器,用 Realm Browser 打开数据库,我们就可以看到我们成功执行了更改:
执行删除操作
断言匹配
我们仍然还想要给这个应用提供一些碉堡的功能,那么快速查找怎么样?在海量的数据中进行查找还是很麻烦的一件事情,但是有了快速查找,一切就都简单了。我们现在所拥有的这个项目已经包含了一个 UISearchController 控件,您所需要做的就是添加一点小小的修改,让这个功能能够在 Realm 中正常工作。
打开 LogViewController.swift ,然后将 searchResults 属性替换为以下代码:
var searchResults: RLMResults!
因为我们仍然是执行 “ 检索 ” 操作,因此我们的数据是存放在 RLMResults 当中的。
向类中添加以下方法:
func filterResultsWithSearchString(searchString: String) { let predicate = "name BEGINSWITH [c]'\(searchString)'" // 1 let scopeIndex = searchController.searchBar.selectedScopeButtonIndex searchResults = SpeciesModel.objectsWhere(predicate) // 2 switch scopeIndex { case 0: searchResults = searchResults.sortedResultsUsingProperty("name", ascending: true) // 3 case 1: searchResults = searchResults.sortedResultsUsingProperty("distance", ascending: true) // 4 case 2: searchResults = searchResults.sortedResultsUsingProperty("created", ascending: true) 5 default: return } }
对应的标号注释如下:
首先我们创建了一个字符串版本的 “ 断言 (predicate)” ,在这里,我们搜索以 searchString 开头的 name 属性。 [c] 可以让 BEGINSWITH 以不区分大小写的灵敏度来进行查找,要注意, searchString 是被单引号括起来的。
我们根据这个断言,使用 objectsWhere 这个方法来执行断言检索操作。
如果选中的标签是 “ 名字 ” ,那么结果就按照 “ 名字 A-Z” 排列
如果选中的标签是 “ 距离 ” ,那么就按照距离排列结果
如果选中的标签是 “ 创建时间 ” ,那么就按照时间来进行排列。
因为搜索会导致表视图调用同样的数据源方法,因此我们需要对 tableView(_:cellForRowAtIndexPath:) 进行小小的修改,以便让其能够处理主要的表视图记录以及查询结果。在这个方法里面,找到以下代码:
speciesModel = species[UInt(indexPath.row)] as! SpeciesModel将其替换为以下代码:
if searchController.active { speciesModel = searchResults[UInt(indexPath.row)] as! SpeciesModel }else { speciesModel = species[UInt(indexPath.row)] as! SpeciesModel }
上面这行代码将会检查 searchController 是否激活。如果激活的话,那么就接收并显示搜索结果的数据;如果不是的话,那么就接收并显示 species 全部数据。
最后,我们需要一个功能,那就是单击范围栏上的按钮时,更变返回结果的排列顺序。
将空 scopeChanged 方法用以下代码来替换:
@IBAction func scopeChanged(sender: UISegmentedControl) { switch sender.selectedSegmentIndex { case 0: species = SpeciesModel.allObjects().sortedResultsUsingProperty("name", ascending: true) case 1: break case 2: species = SpeciesModel.allObjects().sortedResultsUsingProperty("created", ascending: true) default: species = SpeciesModel.allObjects().sortedResultsUsingProperty("name", ascending: true) } tableView.reloadData() }
在上面的代码中,我们将会检查范围栏上的按钮是哪一个被按下( A-Z ,距离,以及添加日期),然后调用 sortedResultsUsingProperty 来进行排序。通常情况下,这个列表将按照名字来排序。
您可能会注意到,现在按照距离排序这一块中目前还是空的( case 1 ),那是因为目前数据模型中还不存在 “ 距离 ” 这么一个玩意儿,因此我们暂时还不需要做这个工作,等到以后添加了再来完善。不过现在,我们已经可以看到它的大致功能了!
编译并运行您的应用,尝试一些搜索操作,然后查看结果!
查看不同的结果
提示:在作者的原教程中,搜索功能实际上是无法实现的。如果您在 “ 合适 ” 的地方添加了相关方法,那么实际上程序仍然还是无法执行搜索功能的。它会提示 cell 的 titleLabel 的值为 nil 。因为在原教程中, Cell 是在 Storyboard 里面自定义的,而搜索栏则是要显示一个新的表视图。如果需要重用自定义的 Cell ,那么最好需要在 Xib 文件中进行制作。因为如果没有 init(style:reuseIdentifier:) 方法的 Cell 自定义类,是无法进行重用的。
更新记录
我们现在已经实现了添加和删除记录的功能了,剩下就是更新数据功能了。
如果您试着单击 LogViewController 中的一个单元格,那么就会跳转到 AddNewEntryViewController 页面,但是这些区域都是空白的。当然,我们首先要做的就是让这个页面显示数据库中存放的数据,以便让用户编辑。
打开 AddNewEntryViewController.swift 文件,然后向类中添加以下方法:
func fillTextFields() { nameTextField.text = species.name categoryTextField.text = species.category.name descriptionTextView.text = species.speciesDescription selectedCategory = species.category }
这个方法将会使用 species 中的数据来填充用户界面的文本框。记住, AddNewEntryViewController 只有在添加新物种时才会保持文本框为空的状态。
接下来,向 viewDidLoad() 方法的末尾添加以下语句:
if species == nil { title = " 添加新的物种 " }else { title = " 编辑 \(species.name)" fillTextFields() }
上面这些代码段设置了导航栏的标题,以通知用户当前其是在添加新的物种还是在更新一个已存在的物种信息。如果 species 不为空,那么就调用 fillTextFields 方法来填充文本框。
现在我们需要一个更新功能,以便响应用户的更改操作。向类中添加以下方法:
func updateSpecies() { let realm = RLMRealm.defaultRealm() realm.beginWriteTransaction() species.name = nameTextField.text species.category = selectedCategory species.speciesDescription = descriptionTextView.text realm.commitWriteTransaction() }
通常情况下,这种方法一般都先获得默认的 Realm 数据库,然后将数据写入的操作放在 beginWriteTransaction() 和 commitWriteTransaction() 方法之间。在这个事务中,我们只是简单的更新了这三个数据域的值。
这六行短短的代码就足以完成 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! let indexPath = tableView.indexPathForSelectedRow() if searchController.active { let searchResultsController = searchController.searchResultsController as! UITableViewController let indexPathSearch = searchResultsController.tableView.indexPathForSelectedRow() selectedSpecies = searchResults[UInt(indexPathSearch!.row)] as! SpeciesModel }else{ selectedSpecies = species[UInt(indexPath!.row)] as! SpeciesModel } controller.species = selectedSpecies } }
我们在这里将选中的物种信息传递给了 AddNewEntryController 。上面的 if/else 代码是因为要根据用户是否是在查看搜索结果来决定的。
编译并运行您的应用,打开记录视图,并且选中一个存在的物种。您应该可以看到文本框中已经填充了数据。
更新数据
把剩下的东西结束掉
要让我们的应用变得更加完美,我们就需要实现剩下 的功能。
还记不记得我们没有变法根据距离来排序记录?我们需要在里面添加不少的代码,才能够正常的运行这个功能,但是这个结果是非常值得的。
打开 Species.swift 文件,然后向类中添加一个新的属性。
dynamic var distance: Double = 0
这个属性为保存用户位置和该记录点的距离信息。然而,没有必要去存储 distance 信息,因为用户位置会随时发生改变。我们想让距离成为这个模型的一部分,但是我们并不想 Realm 来存储这个数据。
Realm 支持一种被称为忽视属性 (ignored properties) 的东西,然后向类中添加以下代码:
func ignoredProperties() -> NSArray { let propertiesToIgnore = [distance] return propertiesToIgnore }
要实现忽视属性,只需要声明一个命名为 ignoredProperties() 的方法,然后返回一个属性数组,里面保存有您不想进行存储的属性。
由于我们并不会存储距离这个属性,很明显地我们需要自己计算距离。
打开 MapViewController.swift ,添加以下方法:
func updateLocationDistance() { let realm = RLMRealm.defaultRealm() if results != nil { for result in results! { let currentSpecies = result as! SpeciesModel let currentLocation = CLLocation(latitude: currentSpecies.latitude, longitude: currentSpecies.longitude) let distance = currentLocation.distanceFromLocation(mapView.userLocation.location) realm.beginWriteTransaction() currentSpecies.distance = Double(distance) realm.commitWriteTransaction() } } }
对于每个物种,我们计算了这个标记点与用户当前位置之间的距离。即时我们没有存储这个距离信息,我们仍然需要将其存储在记录当中,然后将其在写操作事务中保存这个变化消息。
接下来,在 prepareForSegue(_:sender:) 方法底部添加以下代码:
else if segue.identifier == "Log" { updateLocationDistance() }
现在,在用户打开 “ 记录界面 ” 之前,我们需要调用这个方法来计算距离。
接下来,打开 LogViewController.swift ,然后找到 tableView(_:cellForRowAtIndexPath:) 方法。然后在这个方法底部附近, return 语句之前添加以下代码:
if speciesModel.distance < 0 { cell.distanceLabel.text = "N/A" }else { cell.distanceLabel.text = String(format: "%.2fkm", speciesModel.distance / 1000) }最后,找到 scopeChanged() 然后将 case 1 中的 break 替换成以下代码:
species = SpeciesModel.allObjects().sortedResultsUsingProperty("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 的更多信息,您可以查看 官方文档 ,我发现这个文档真的写得十分详尽。
如果您对本教程有什么建议和意见,请到评论区进行讨论,我会尽快处理这些建议和意见的 ~
原文作者 Bill Kastanakis
译者及改编 星夜暮晨(QQ:412027805)