2

使用Realm和Swift编写一个ToDo应用

作者:HOSSAM GHAREEB,时间:2015/11/28
翻译:BigNerdCoding, 如有错误欢迎指出。原文链接

在去年智能手机的大更新之后,很多工具也同时被开发出来了。这些工具让我们开发者发布一个高性能、高质量的应用的变的更加简单了。在应用商店获得高排名以及不再很容易。而且让应用更容易拓展也是很困难的一件事。当你的应用成功的拥有百万级别的用户的时候,你需要注意应用中的所有事情以及所有操作。因此,现在每个开发者都需要面临的一个问题就是处理数据库。而这又是一件让人感到非常头疼的事情,大多数的开发者会在SQLiteCore Data中挑选一个。曾经我是Core Data的拥趸,因为它在处理数据以及持久化数据方面功能非常强大。但是后来我发现使用Core Data会浪费很多时间。现在我会使用Realm,该框架能够很好的替换SQLiteCore Data

Realm是什么

Realm是一个跨平台的手机端数据库支持iOS(Swift和Object双语言版本)、安卓。相比于SQLiteCore Data更好也更快。除此之外,它的使用也很方便之需要几行代码就可以搞定。Realm是一个开源产品你可以免费试用。Realm之所以会出现是因为在过去的十年中移动数据库没有任何可喜的更新。过去在处理移动数据库的时候,你几乎只能选择SQLite或者在其基础上进行封装的Core Data。因为Realm并不是一个ORM(对象关系映射)且有自己的持久化引擎使得Realm容易使用并且拥有很好的性能和速度。

为什么选择Realm

Realm快的难以置信并且易用,你能看见任何你需要的东西,并且只需要几行代码就可以完成数据库的读写操作。这里我会列出所有在移动端使用Realm的好处和理由:

  • 安装简单:在后面你会发现安装Realm比你想象的还要简单,之需要在Cocoapods中添加简单的命令就能使用了。

  • 速度:Realm远快过于SQLiteCore Data,官方提供的比较证据

  • 跨平台:Realm的数据库文件是跨平台的,它可以在iOS和Android中进行分享。无论你是使用Java、Object-C、还是Swift,你都可以使用高级的模型。

  • 可拓展性:当你的手机应用拥有大量的用户以及数据记录的时候可拓展就是很重要的一个特征。拓展性问题从一开始设计和选择工具的时候就需要进行认真的考虑。Realm在能够高效处理大数据量的同时依然拥有着非常好的拓展性。在应用中引入该框架会让程序的速度得到提升。

  • 良好的文档支持:Realm团队提供了可读性强、组织良好的的丰富文档给大家。如果你依旧有问题解决不了的话,可以在 Twitter、Github、Stackoverflow上去向它们寻求帮助和解答。

  • 可靠:Realm依旧被大量的创业团队和公司的移动应用使用像:Pinterest、Dubsmash、Hipmunk。

  • 免费:如此强大,而且还是完全免费的。

开始干活

让我们Realm使用教程,并用它创建一个Swift语言版本的iPhone简单Todo应用。用户在该应用中可以添加多个任务链表,每个链表里面又会有多个任务。每个任务都有一个标题、备注、到期时间,一个图像附件以及一个标记是否完成的标记量。在开始编写工程之前我们首先需要配置Xcode并安装Realm工作所需的一个工具。

需要的条件

下列条件必须满足:

  • iOS 8 or later、OS X 10.9 or later。

  • Xcode 6.3 or later。

  • Realm的有两个Swift版本,一个是2.0版本另一个是1.2版本。我们在教程中使用的是2.0版本。你也可以选择使用1.2版本的,但是该版本在未来不会被维护和支持,因此最安全的办法就是使用2.0版本。

配置Xcode并安装工具

再开始配置Xcode之前请确保你已经安装了CocoaPods,我们需要使用它在Xcode工程中安装Realm。如果你对CocoaPods不熟悉的话,你可以去官网操作安装教程。

现在,我们创建一个"Single View Application"模版的工程,并将工程命名为“RealmTasks”或者你喜欢的名称。请确保使用的是Swift语言。接下来我们在终端中切换到当前工程的目录并按照下面步骤初始化工程的CocoaPods。

pod init

使用编辑器生成的文件podfile,并在文件中添加如下内容:

clipboard.png

接下来运行命令"pod install"去下载安装Realm到你的工程里面。当安装完成后,你会发现文件夹下面又一个新的Xcode workspace被创建了。打开RealmTasks.xcworkspace文件,你会看见如下界面:

现在Realm已经能够使用了,但是我们还是安装一些工具类帮助我们更加容易的使用Realm。

安装Realm插件

Realm团队为Xcode提供了很好的插件,该插件能够创建Realm模型。我们使用Alcatraz来安装这个插件。该工具可以很好的帮助你自动安装那些开源的插件,模版、颜色主题。对于那些不知道Alcatrza的开发者来说,这可以节省很多的时间和精力。直接使用下面的命令安装Alcatrza:

curl -fsSL https://raw.githubusercontent.com/supermarin/Alcatraz/deploy/Scripts/install.sh | sh

接下来在Xcode中选择Window菜单栏下面的Package Manager,如下图:

clipboard.png

在弹出的窗口中选择你需要安装的类型,并在搜索框中输入对应的插件、模版或者主题。我们选择Plugins,输入"Realm",在出现的结果里面选择"RealmPlugin"并安装。如下图:

clipboard.png

此处可能在Xcode7.1以上版本会出现一些问题,解决方法

Realm Browser

最后一个工具是Realm Browser。该浏览器可以帮助你查看或者编辑你的.realm数据库文件。这些数据文件在你的应用中被创建出来,并且包含了里面的实体、属性、以及数据表中的纪录。这些文件如之前所说的一样可以在像iOS、Android这样不同的平台之间分享。你可以在iTunes store下载到最新版本的工具。打开该应用选择Tools -> Genetate demo database,应用会为你新建一个测试数据库文件你可以在浏览器中看到所有的纪录。如下图:

clipboard.png

正如上图显示的,类RealmTestClass1有1000条纪录以及不同类型的参数(列)。我们会在下面接受它支持的类型。

现在一切准备工作都已经完成了。开始编码吧。

数据库Model类

游戏开始了!首先我们需要新建一个模型类。可以通过创建一个继承与Object的Swift类。考虑到Object是所有Realm model类的基类,你可以拓展任何拓展自Obeject的Realm model类。当你创建自己的类的时候,理所当然你需要定义属性。Realm支持下面各种类型的属性:

  • Int, Int8, Int16, Int32, and Int64

  • Boolean

  • Float

  • String

  • NSDate

  • NSData

  • Class extends Object => Used for One-to-one relations

  • List<Object> => Used for one-to-many relations

List在Realm类中表示对象实例的集合,就像上面演示数据库截图表示的那样。截图中的最后一列就是一个存在于另一张表中纪录指针的数组。在使用Realm模型类的时候,你可以像对待其他Swift类一样对待它。例如,你可以在类里面添加函数方法,协议。

Talk is cheap,show me the code ?

我们使用刚才安装的Realm插件创建一个Realm类。在Xcode中新建文件,在左侧选择Realm。如图:

clipboard.png

选择Swift语言,类名为Task。如下图:

clipboard.png

现在为该类添加属性。

属性

我们需要在Task类中添加属性,每一个Task都会有名称、创建日期、备注、是否完成。添加完成之后代码如下:

class Task: Object {
    
    dynamic var name = ""
    dynamic var createdAt = NSDate()
    dynamic var notes = ""
    dynamic var isCompleted = false
    
    
// Specify properties to ignore (Realm won't persist these)
    
//  override static func ignoredProperties() -> [String] {
//    return []
//  }
}

你可以发现添加的所有属性都被声明为dynamic var,之所以这样是为了让这些属性能够被底层数据库数据访问到。

接下来,我们定义一个TaskList类,该类存储多个任务:

class TaskList: Object {
    
    dynamic var name = ""
    dynamic var createdAt = NSDate()
    let tasks = List<Task>()
    
// Specify properties to ignore (Realm won't persist these)
    
//  override static func ignoredProperties() -> [String] {
//    return []
//  }
}

TaskList类有名称、创建时间、任务链表。下面是一些说明补充:

  • List<Object>对应一个任务列表有多个任务这种一对多的关系。

  • List于数组的类似,用户可以通过下标索引来访问链表中的数据。注意:链表中的数据必须是用一个类型。

  • List<T>是一个泛型数据类型,之所以不在该泛型属性前面添加dynamic声明是因为泛型属性无法通过Objective-C的运行时表示。

Realm中关系的建立就像你前面看到的一对多的实现一样简单直接。一个简单的一对一的例子如下:

class Person: Object{
    dynamic var name = ""
}

class Car: Object{
    dynamic var owner:Person?
}

上面的示例代码很好的表现了一对一的关系:每个人都有一个对应的车主。

到目前为止,我们已经建好了基础的model类。接下来我们继续创建Todo应用的教程。首先,下载代码在Xcode7或者更高的版本中运行,你会看见下面截图一样的界面:

clipboard.png

在这个工程中,我添加了两个视图控制器:TasksViewController和TaskListViewController。前面一个视图控制器是用来展示一个任务的细节,第二个视图控制器是用来显示所有的任务。在列表视图中你点击+按键添加一个任务列表。选择一个视图列表会跳转到另一个视图中添加多个任务。

带着演示应用的基本概念,现在让我们看看如何添加一个任务链表到Realm数据库中。为了实现这个功能,我们需要解决下面两件事:

  • 创建一个新的TaskList model对象并将其保存到Realm.

  • 使用查询语句从数据库中读出数据并更新界面UI。

为了将对象保存到Realm,你所需要做的就是实例化Obeject子类的model对象并将其写入到数据库。下面就是代码示例:

let taskListA = TaskList()
taskListA.name = "Wishlist"
        
let wish1 = Task()
wish1.name = "iPhone6s"
wish1.notes = "64 GB, Gold"
        
let wish2 = Task(value: ["name": "Game Console", "notes": "Playstation 4, 1 TB"])
let wish3 = Task(value: ["Car", NSDate(), "Auto R8", false])

taskListA.tasks.appendContentsOf([wish1, wish2, wish3])

我们创建了一个任务链表,并使用初始化方法进行了实例化设置了部分属性。然后我们创建了三个task类型的对象(wish1, wish2 and wish3)。在这里我使用了三种方法来创建Realm对象:

  1. 使用Realm类的实例化方法创建wish1并设置属性

  2. 通过传递键值类型的字典类型的属性来创建wish2。

  3. 通过传递一个数组来创建wish3。数组中的值与类中声明的属性顺序一样。

嵌套对象

Realm中另一个创建对象的方法就是嵌套对象。该方法在对象关系是一对一或者一对多的时候可以使用(意味着你有一个Object类型的属性或者一个List<Object>类型的属性)。如果你使用了上面的方法2或者方法3的话,你可以使用一个表示属性的数组或者字典来取代该方法,代码如下:

let tasklistB = TaskList(value: ["MoviesList",NSDate(), [["The Martian", NSDate(), "", false], ["The Maze Runner", NSDate(), "", true]]])

在上面的代码中,我们新建了一个电影链表,设置了名称、时间、任务数组。每一个任务又是通过属性数组创建的。例如[“The Maze Runner”, NSDate(), “”, true]就表示名称、时间、备注、是否已经完成了。

持久化Realm中的对象

现在你知道了如何创建并使用Realm对象。但是为了在应用重新启动的时候依旧能够使用这些对象,你需要通过Realm数据库的写事务来进行对象持久化。一旦对象数据被持久化了并存在于Realm数据库中,你就可以在任何线程中访问这些对象。为了执行这个写事务,你需要一个Realm对象。一个Realm的实例就代表一个Realm数据库。你可以如下创建该实例:

let uiRealm = try! Realm()

我们将该实例定义在AppDelegate.swift文件的上面这样就可以在所有的文件中使用了。后面你可以如下简单的调用写方法:

uiRealm.write { () -> Void in
            uiRealm.add([taskListA, taskListB])
}

首先uiRealm对象在AppDelegate类中创建好了并且能够在整个应用中使用。每一个线程里面Realm对象只能创建一次,因为Realm对象不是线程安全的并且不能在不同的线程中共享。如果你想在别的线程里面执行写事务,你需要创建一个新的Realm对象。这里创建的uiRealm对象就是在UI线程中使用的。

现在我们回到app中,当用户点击创建按键的时候我们需要保存一个task lists。在TasksViewController文件的displayAlterToAddTask方法中,我们如下创建对象:

let createAction = UIAlertAction(title: doneTitle, style: UIAlertActionStyle.Default) { (action) -> Void in
    
    let taskName = alertController.textFields?.first?.text
    
    if updatedTask != nil{
        // update mode
        uiRealm.write({ () -> Void in
            updatedTask.name = taskName!
            self.readTasksAndUpateUI()
        })
    }
    else{
        
        let newTask = Task()
        newTask.name = taskName!
        
        uiRealm.write({ () -> Void in
            
            self.selectedList.tasks.append(newTask)
            self.readTasksAndUpateUI()
        })
    }

}

在上面的代码中,我们从text field中获取名称,然后调用Realm的写方法保存该任务链表。

请注意:当有多个线程同时执行写操作的事务时,它们都会阻塞对方的线程并且也会阻塞自己所在的线程。所以你应该在一个单独的线程操作里面执行写事务而不是在UI线程里面。另一件事时:读操作不会阻塞写事务的操作。这在用户浏览应用并伴随很多读操作的同时进行后台数据写事务时很有帮助的。

检索对象

你已经知道在Realm中进行数据写操作了,但是如果你不知道如何检索处这些数据那么写操作也就没有意义了!其实Realm中查询很简单直接。你传递一些自定义的查询条件,然后执行查询,筛选出来的结果就会展示在你的面前。你可以将查询得到的结果看成时Swift中的数组,因为它们有着相似的接口。

当你实例化一个结果对象之后,很容易就能获取磁盘中的数据。事务中对数据的任何修改都会直接影响磁盘中的数据。在Realm中你可以通过调用以类名作为参数的方法得到结果。下面我们看看如何读出TaskLists并更新UI:

我们已经在TasksListsViewController定义了属性:


var lists : Results<TaskList>!

readTasksAndUpdateUI方法的实现:

func readTasksAndUpdateUI(){
    
    lists = uiRealm.objects(TaskList)
    self.taskListsTableView.setEditing(false, animated: true)
    self.taskListsTableView.reloadData()
}

在tableView(_:cellForRowAtIndexPath:_) 方法里面,我们显示任务链表名以及任务数:

func tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
    
    let cell = tableView.dequeueReusableCellWithIdentifier("listCell")
    
    let list = lists[indexPath.row]
    
    cell?.textLabel?.text = list.name
    cell?.detailTextLabel?.text = "\(list.tasks.count) Tasks"
    return cell!
}

是不是很简单?最后我们需要在viewWillAppear中调用readTasksAndUpdateUI函数,确保每次视图出现的时候都是最新的。

override func viewWillAppear(animated: Bool) {
    readTasksAndUpdateUI()
}

以上就是使用Realm进行读写task lists的内容了。下面,我们需要知道Realm中如何进行删除和更新操作。在开始之前,我们先来看看工程中lists的编辑、删除操作的代码。

首先在TaskListsViewController中有一个isEditingMode的布尔变量,用于编辑和正常模式的切换。

var isEditingMode = false

当点击编辑按键的时候,didClickOnEditButton方法将被调用:

@IBAction func didClickOnEditButton(sender: UIBarButtonItem) {
    isEditingMode = !isEditingMode
    self.taskListsTableView.setEditing(isEditingMode, animated: true)
}

该动作使用table view中的setEditing方法来设置是否处于编辑模式。在table view中编辑模式下的默认方法就是删除改单元,但是在iOS8.0的UITableViewDelegate中引入了一个editActionsForRowAtIndexPath方法,该方法用于当用户滑动单元格的时候定制自己的方法。

我们添加删除、编辑两个方法,实现如下:

func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
    let deleteAction = UITableViewRowAction(style: UITableViewRowActionStyle.Destructive, title: "Delete") { (deleteAction, indexPath) -> Void in
        
        //Deletion will go here
        
        let listToBeDeleted = self.lists[indexPath.row]
        uiRealm.write({ () -> Void in
            uiRealm.delete(listToBeDeleted)
            self.readTasksAndUpdateUI()
        })
    }
    let editAction = UITableViewRowAction(style: UITableViewRowActionStyle.Normal, title: "Edit") { (editAction, indexPath) -> Void in
        
        // Editing will go here
        let listToBeUpdated = self.lists[indexPath.row]
        self.displayAlertToAddTaskList(listToBeUpdated)
        
    }
    return [deleteAction, editAction]
}

我们通过带有style、title、handler的UITableViewRowAction添加了两个action。现在当你向左滑动或者点击编辑的时候界面会是下面这样:

clipboard.png

上面是关于UI如何响应删除和更新动作的。

删除对象

为了删除Ralm数据库中的对象或者数据,你可以通过传递需要的对象给delete方法。当然,改操作应该在一个写事务内部。下面的代码就是删除操作的代码实现:

let listToBeDeleted = self.lists[indexPath.row]
uiRealm.write({ () -> Void in
           uiRealm.delete(listToBeDeleted)
           self.readTasksAndUpdateUI()
    })
    

当删除对象后调用readTasksAndUpdateUI方法来更新界面。

出了上面的删除一个对象,还可以调用Realm中的deleteAll方法来删除数据库中索引类和数据。该方法在当前用户退出应用而你需要清除数据库中的所有持久化数据的时候非常有用。

uiRealm.write({ () -> Void in
    uiRealm.deleteAll()
})

更新对象

在Realm中有很多方法可以实现对象的更新操作,但是所有的的这些方法都必须在一个写事务里面。下面我们将会看见其中的一些更新对象方法。

使用属性

你可以通过在写事务的闭包里对对象的属性的值进行重新设置来完成Realm对象的更新。例如,在TasksViewController中我们可以通过下面的方法来改变一个任务的完成状态:

uiRealm.write({ () -> Void in
    task.isCompleted = true
})

使用主键

Realm支持使用一个字符串或者整型属性来作为对象的主键。当用户通过add()方法来创建新的Realm对象的时候,如果对象的键值以及存在,那么对象会被更新为一个新的值。下面是示例:

let user = User()
user.firstName = "John"
user.lastName = "Smith"
user.email = "example@example.com"
user.id = 1
// Updating User with id = 1
realm.write {
            realm.add(user, update: true)
        }
       

上面的id属性是键值,如果已经存在一个id = 1的用户的时候,Realm会相应的更新该对象。否则直接插入。

使用KVC(Key-Value Coding)

如果你是一个有经验的iOS开发人员,你一定会对KVC很熟悉了。Realm中的Object、Results、List类都兼容KVC。该特性能让你在运行时设置更新属性。另一个非常好的特性是对于List、Results你可以以集合的方式批量进行更新,而无需迭代每一个来进行更新。可能你还不是很明白,看下面的例子:

let tasks = uiRealm.objects(Task)
uiRealm.write { () -> Void in
    tasks.setValue(true, forKeyPath: "isCompleted")
}           

在上面的代码中,我首先查询得到了所有任务对象然后将所有结果的isCompleted设置为了true。这意味着我只使用了一行代码就完成了对所有任务的完成标记。

我们再次回到ToDo app,在displayAlertToAddTaskList方法中你应该能够发现如下的代码片段:

 //update mode
 uiRealm.write({ () -> Void in
           updatedList.name = listName!
           self.readTasksAndUpdateUI()
 })
 

该代码会在用户编辑list名称的时候执行。我们通过设置属性名来完成更新操作。

显示Tasks

我们已经看过了TaskListViewController中的绝大部份代码。现在我们来看下用于显示一个任务列表任务的TasksViewController。该视图控制器有一个UITableView,这个table view分为了两个部分:待完成和已完成任务。在TasksViewController中有如下属性:

var selectedList : TaskList!
var openTasks : Results<Task>!
var completedTasks : Results<Task>!

selectedList是通过TaskListsViewController传递过来的选择的任务链表。为了区分任务的完成状态,定义了两个变量openTasks、completedTasks。使用Realm的神奇函数filter()可以实现区分。在解释如何筛选前,先看下代码:

func readTasksAndUpateUI(){
    
    completedTasks = self.selectedList.tasks.filter("isCompleted = true")
    openTasks = self.selectedList.tasks.filter("isCompleted = false")
    
    self.tasksTableView.reloadData()
}

在这个函数里面我们通过使用Realm提供的filter()方法涉嫌了结果的区分筛选。该方法能被 List、Result、Object实例进行调用,并根据设置的字符串条件返回我们期待的结果。你可以将该函数想象成NSPredicate,两个基本上是一样的功能。你也可以通过筛选条件创建NSPredicate来完成一样的功能。

下面是一个示例:

//using predicate string
var redCars = realm.objects(Car).filter("color = 'red' AND name BEGINSWITH 'BMW'")

// using NSPredicate
let aPredicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "red", "BMW")
redCars = realm.objects(Car).filter(aPredicate)

在上面的代码中,我们筛选了那些红色并且名字以"BMW"开头的车。第一行和第二行代码筛选出来的结果是一样的。下面的表中列出了常用的筛选比较操作符:

clipboard.png

排序

到目前为止我已经解释了Realm数据库的基本操作,在结束教程之前还有一个特征我想介绍给大家。排序是Realm提供的另一个非常有用的特性。在List、Result中你可以调用排序算法("sore criteria")对一组数据进行排序。下面我们看看如何使用字母或者创建时间来进行排序。首先我们在UI上添加一个segmented控件,排序会依据用户进行。

clipboard.png

代码实现:

@IBAction func didSelectSortCriteria(sender: UISegmentedControl) {
        
        if sender.selectedSegmentIndex == 0{
            
            // A-Z
            self.lists = self.lists.sorted("name")
        }
        else{
            // date
            self.lists = self.lists.sorted("createdAt", ascending:false)
        }
        self.taskListsTableView.reloadData()
}

总结

Realm是一个简单直接的本地存储管理和数据库的解决方案。Realm让你只需要几行代码就能让代码变得可拓展型强、且简化了工作节省了时间。对于那些需要使用数据库的应用和公司来说,Realm真的很值得一试。

接下来要做的

这篇教程只是简单介绍了Realm的一些基本操作,例如Reading、Writing、Updating、Deletion。还有一些更高级的话题很值得你自己去探索学习,最好的方法就是去官方网站看官方文档。

整个演示的完整代码


BigNerdCoding
1.2k 声望125 粉丝

个人寄语: