用 SwiftyDB 管理 SQLite 数据库

原文链接:http://www.appcoda.com/swiftydb/
作者:AppCoda
原文日期:2016-03-16
译者:Crystal Sun

选择哪种数据持久化的方式,是我们在开发 App 时常常遇到的问题。我们有太多选择了:创建一个单独的文件,使用 CoreData,或者创建 SQLite 数据库。而使用 SQLite 数据库会有一些麻烦,因为首先要先创建数据库,提前写好表和字段,此外,从编程的角度来看,数据的存储、更新、和获取都不是很容易操作。

而当我们使用 GitHub 上的 SwiftyDB 这个第三方库时,上面的这些问题似乎都轻而易举地解决了。SwiftyDB 用作者的话来说,就是即插即用型的好帮手。SwiftyDB 将开发者从繁重的手动创建 SQLite 数据库的工作中解放出来,再也不用提前定义好各种表和字段了。SwiftyDB 中类的属性能够自动完成上述工作,可以直接用类作为数据模型。除此之外,所有对数据库的操作都藏在背后了,所以开发者可以把所有的注意力放到应用的逻辑层面上。简单强悍的 API 可以让处理数据成为小菜一碟的事情。

不过有必要提及一下,不要期望 SwiftyDB 可以创造奇迹,它就是一个靠谱的第三方库,可以把一些它该做的事情做到非常漂亮,不过还有一些特性可能将来才会拥有,然而,它仍然是一个非常好用的工具,值得获得你的注意,所以在本篇文章中,我们将讲述 SwiftyDB 的基本使用操作。

可以从这里找到文档供你参考,在你看完这篇文章后,你绝对需要看一下这个链接。如果你一直想用 SQLite 数据库,不过犹豫至今也没有开始,我相信,SwiftyDB 是一个好的开始。

基于上面所说的,让我们开始探索这个全新的、令人期待的工具吧。

关于 Demo App

在我们今天的这篇文章中,我们要创建一个非常简单的笔记应用,可以做如下基本的操作:

  • 列出笔记
  • 创建新的笔记
  • 更新已经创建的笔记的内容
  • 删除笔记

很明显,SwiftyDB 将要管理一个 SQLite 数据库,上面列出的操作足以向你展示如何开始使用 SwiftyDB。

为了更方便的开始,我事先创建了一个工程,点击下载然后打开工程。用 Xcode 打开工程后,能够看到所有的基本功能,不过与数据有关的代码都缺失了,然后运行一次工程,你就能看到全貌了。

应用有一个导航栏,在第一个 view controller 中,有一个 tableview,列出来了所有的笔记。

点击某个笔记,我们可以编辑更新内容,如果向左滑动某条笔记,可以删除笔记:

创建一个新的笔记只需要点击导航栏上的加号按钮,为了能够更好的做出示例,下列列出了我们在编辑笔记时可以进行的操作:

  1. 设置笔记的标题和内容。
  2. 更改字体。
  3. 更改字体的大小。
  4. 更改字体的颜色。
  5. 添加图片。
  6. 移动图片到另外一个位置。

上述所有的值的改变都会存储到数据库中。为了让最后两部分更清楚一些,图片实际上是存储在应用的 documents directory 中,我们在数据库中只是存储图片的名字和 frame。不仅如此,我们还要创建一个类来管理图片(更多细节参见后面的内容)。

最后一点也是最重要的一点,你刚刚下载了初始工程,不过到了下一部分,你会有一个 workspace,因为我们要使用 CocoaPods 来下载 SwiftyDB 的库,以及附加的其他的 dependencies。

当年准备好了就赶紧开始吧,不过首先,如果你在 Xcode 中打开了刚刚下载的初始工程,那么请先关闭。

安装 SwiftyDB

第一件事情就是下载 SwiftyDB 的库,然后在工程中使用。下载库的文件然后放到工程中可不管用,我们要先安装 CocoaPods。安装过程不复杂,不会花费太多时间,即使你从来没有用过 CocoaPods。仅供参考,看一眼之前给的 CocoaPods 的链接。

安装 CocoaPods

我们要将 CocoaPods 安装到系统中,如果你已经安装了 CocoaPods,那么请跳过这一步,如果没有,那么继续跟着我走,打开 Terminal 终端 ,输入下列命令:

sudo gem install cocoapods

然后按回车,输入 Mac 密码,等一会然后开始下载,下载完毕后不要关闭 Terminal 终端 ,我们之后还会用到。

安装 SwiftyDB 和其他的 Dependencies

使用 cd 命令找到初始工程对应的文件夹(仍然是在 Terminal 终端中进行操作)。

cd PATH_TO_THE_STARTER_PROJECT_DIRECTORY

现在可以创建 Podfile 文件了,我们在 Podfile 里写出我们需要的下载的库。最简单的方法是输入下列命名,让 CocoaPods 给我们创建一个 Podfile。

pod init

一个名为 Podfile 的文件就创建好了,在工程文件夹里,打开 Podfile,最好使用文本编辑软件(最好不要用 TextEdit 这个软件),然后将内容修改成:

use_frameworks!

target 'NotesDB' do
    pod "SwiftyDB"
end

这行代码实际上就做了 pod "swiftyDB" 一件事。CocoaPods 会下载 SwiftyDB 库和所有的 dependencies,还会创建一些新的子文件夹,以及一个 Xcode workspace。

一旦你编辑完 Podfile 文件,保存关闭。确保你关闭了初始工程,回到 Terminail 终端上,输入下列命名:

pod install

等一会,然后就可以继续了。我们这次不再打开初始工程,而是打开 NoteDB.xcworkspace

开始使用 SwiftyDB - 我们的 Model

NotesDB 工程中,有个文件叫做 Note.swift,当前里面还是空的。这就是我们今天要讲述的重点内容,我们要创建一些类,表示一条笔记的 entity,在理论层面上,即将完成的工作就是 iOS MVC 模式里的 Model

首先需要引入 SwiftyDB 库,到文件的顶部输入下面这行代码:

import SwiftyDB

现在,声明最重要的一个类:

class Note: NSObject, Storable {

}

我们使用 SwiftyDB 库时,需要遵循几条规则,在上面的类开头一行,可以看到这其中两条规则:

  1. 带有属性的类要用 SwiftyDB 存到数据库里必须是 NSObject 类的子类
  2. 带有属性的类要用 SwiftyDB 存到数据库里必须必须遵守 Storable 协议(也是一个 SwiftyDB 协议)。

现在,我们要想一想在这个类里,我们需要哪些属性,这就需要了解 SwiftyDB 的一条新规则:当从数据库中获取数据时,datatypes 属性必须是这里列出的一种,以便能载入整个 Note 对象,而不是简单数据(比如一个都是字典的数组)。如果有某个属性是“不兼容的”数据类型,那么我们就需要额外做一些操作,把它们转换成建议的类型(我们在之后会进行详细的说明)。默认情况下,将数据存储到数据库时,不兼容的数据类型的数据都会被 SwiftyDB 直接忽略掉。也不会创建对应的表单。同样的,对于我们不想存储到数据库中的其他属性,我们也会特殊对待的。

目前需要说明的最后一条要求就是,遵守 Storable 协议的类必须执行 init 方法:

class Note: NSObject, Storable {
    
    override required init() {
        super.init()
        
    }
}

现在,我们已经有所需的信息了,让我们开始声明类的属性吧。目前位置这不是所有的属性,有些有待以后讨论,然而,下面这些是基本的属性:

class Note: NSObject, Storable {
    
    let database: SwiftyDB! = SwiftyDB(databaseName: "notes")
    var noteID: NSNumber!
    var title:String!
    var text:String!
    var textColor: NSData!
    var fontName:String!
    var fontSize:NSNumber!
    var creationDate:NSDate!
    var modificationDate:NSDate!
    
    ...
}

除了第一个之外,其他的没必要详细说了。当对象初始化后,就会创建一个新的数据库(名为 notes.sqlite),如果数据库不存在,那么就会自动创建一个表,表单会和拥有正确数据类型的属性匹配。换句话说,如果数据库已经存在了,就会直接打开数据库。

你可能会注意到,上面的属性都是描述一条笔记和所有我们想存储的特性(标题、问题、文字颜色、字体和大小、创建和修改日期),不过这里没有笔记中存储的图片。哈哈,我是故意的,我要给图片创建一个类,只存储两个属性:图片的名字和尺寸。

所以,继续在 Note.swift 文件中创建下列类,放到之前的那个类的上方或者下方皆可:

class ImageDescriptor: NSObject, NSCoding {
    
    var frameData: NSData!
    var imageName: String!
    
}

注意在类里,图片的 frame 是一个 NSData 对象,不是 CGRect 对象。有必要这样操作,因为这样我们可以非常容易的将值存储到数据库里。过一会你就会看到我们是如何搞定的,你也就明白为什么我们要使用 NSCoding 协议。

回到 Note 类,让我们声明一个 ImageDescriptor 数组,如下文:

class Note: NSObject, Storable {
    ...
    
    var images: [ImageDescriptor]!
    
    ...
}

这里有一个限制,现在是时候提到它了,就是实际上 SwiftyDB doesn't store collections to the database。简单的来说,就是我们的 images 数组永远不会被存储到数据库里,我们不得不解决如何处理图片存储的问题。我们可以使用所支持的数据类型中的一个(看我提供的连接,然后再开始这一部分),而最合适的数据类型是 NSData。所以,我们不会把 images 数组存储到数据库里,而是存储下列新的属性:

class Note: NSObject, Storable {
    ...
    
    var imageData:NSData!
    
    ...
}

但是我们如何才能将带有 ImageDescriptor 对象的 images 数组变成 imagesData``NSData 对象呢?恩,答案就是 archiving 这个 images 数组,通过使用 NSKeyedArchiver 类,然后生成 NSData 对象。我们在后面会演示如何用代码实现,不过现在,我们要知道哪些我们事情不得不做,我们需要回到 ImageDescriptor 类里,添加一些东西。

正如你所知的,一个类可以被 archived(在其他编程语言中也就做 serialized),只要类的所有属性都可以被 serialised 就可以了。在我们的例子中,这是可行的,因为ImageDescriptor 类里的这两个属性的数据类型(NSDataString)是可以被 serialesed。然而这还不够,因为我们还必须要 encodedecode 它们,以便于能成功地分别 archive 和 unarchive,这也就是我们为什么需要 NSCoding 协议的原因。有了 NSCoding 协议,我们可以引进如下方法(其中一个就是 init 方法),我们能恰当地 encode 和 decode 我们两个属性:

class ImageDescriptor: NSObject, NSCoding {
    ...
    
    required init?(coder aDecoder: NSCoder) {
        frameData = aDecoder.decodeObjectForKey("frameData") as! NSData
        imageName = aDecoder.decodeObjectForKey("imageName") as! String
    }
    
    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeObject(frameData, forKey: "frameData")
        aCoder.encodeObject(imageName, forKey: "imageName")
    }
    
}

更多关于 NSCoding 协议和 NSKeyedArchiver 类的信息请参见这个地方这个地方,我们现在没有必要在这里讨论了。

除上述所说外,让我们定义一个便利的自定义的 init 方法。非常简单,所以就没有必要做过得解释了:

class ImageDescriptor: NSObject, NSCoding {
    ...
        
    init(frameData: NSData!, imageName: String!) {
        super.init()
        self.frameData = frameData
        self.imageName = imageName
    }   
}

在这一部分我们快速浏览了 SwiftyDB 库,尽管我们不太知道 SwiftyDB,但是这部分还是非常有必要的,主要有三个原因:

  1. 为了创建一个能使用 SwiftyDB 库的类。
  2. 为了能够了解一些在使用 SwiftyDB 库时的规则。
  3. 为了能够了解一些有关数据类型的限制要求,哪些数据类型可以被存储到 SwiftyDB里。

注意:如果你在 Xcode 中看到了一些错误提示,立即 Build 工程(Command + B),错误提示就会消失了。

关键的键值和忽略的属性

在和数据库打交道时,强烈推荐使用 primary keys,因为这些 keys 能够帮你在数据库表中创建独一无二的标识符,进行各种各样的操作(例如,更新某个数据)。你可以从这里找到有关 primary key 的定义。

在 SwiftyDB 数据库中,将类中的某个或某些属性定义为 primary key 的操作非常简单,库里提供了 PrimaryKeys 协议,这样被引入到所有的类中,对应的表都会有一个 primary key,所以对象就有了独一无二的标识符。方法非常直接简易明了,所以让我们马上开始吧:

NotesDB 工程中找到文件名为 Extensions.swift 的文件,点击打开,加入下列代码:

extension Note: PrimaryKeys {
    class func primaryKeys() -> Set<String> {
        return ["noteID"]
    }
}

在我们的 demo 里,我想让 noteID 属性成为在 sqlite 数据库对应的表里唯一的 primary key。然而,如果需要更多的 primary keys,那么你只需要用逗号分隔即可(比如,return ["key1","key2","key3"])。

除此之外,并不是类中所有的属性都要存储到数据库中,你应该明确指出让 SwiftyDB 知道哪些不存储。例如,在 Note 类中,我们有两个属性是不存储到数据库里的(要么就是不能被存储,要么就是我们不想存储):images 数组和 database 对象。我们如何明确地排除这两个属性呢?通过引入 SwiftyDB 提供的另外一个协议,叫 IgnoredPropertie

extension Note: IgnoredProperties {
    class func ignoredProperties() -> Set<String> {
        return ["images","database"]
    }
}

如果还有更多属性我们不想存储到数据库中,那么也需要添加到上面的代码中,例如,结社我们有这么一个属性:

var noteAuthor: String!

我们不想把它存储到数据库中,这样的话,我们需要把这个属性添加到 IgnoredProperties 协议里:

extension Note: IgnoredProperties {
    class func ignoredProperties() -> Set<String> {
        return ["images","database","noteAuthor"]
    }
}

保存一个新的笔记

我们在 Note 里已经完成了大部分的引入,是时候回到 demo app 的功能上来了。我们还没有给我们新的类添加任何方法呢,我们接下来就做这件事情,补全所有缺失的功能。

首先要有笔记,需要告诉 App 如何正确地使用 SwiftyDB 来保存笔记和两个新创建的类。大部分的操作会在 EditNoteViewController.swift 中实现,打开此文件,在写代码之前,我先列出几条特别重要的属性:

  • imageViews:这个数组里有所有的 image view 对象,对象里有所有添加到笔记的图片。这个数组已经存在了,过会就能发现它的强大作用。
  • currentFontName:里面有应用于文本的字体名字。
  • currentFontSize:里面是文本的字体的字号。
  • editedNoteID:即将更新内容的笔记的 noteID 值(primary key)。一会儿我们就会用到。

基础的功能已经在初始工程中提前写好了,我们需要做的就是补全缺失的 saveNote() 方法中的逻辑。首先做两件事情:一、如果笔记没有标题或者笔记没有内容,那么,不允许用户保存笔记。二、在保存笔记时,隐藏键盘。如下:

    func saveNote() {
        if txtTitle.text?.characters.count == 0 || tvNote.text.characters.count == 0 {
            return
        }
        
        if tvNote.isFirstResponder() {
            tvNote.resignFirstResponder()
        }
         
    }

继续初始化一个新的 Note 对象,给各个属性赋值。images 属性需要特殊对待,我们在后边再处理。

    func saveNote() {
        ...
                
        let note = Note()
        note.noteID = Int(NSDate().timeIntervalSince1970)
        note.creationDate = NSDate()
        note.title = txtTitle.text
        note.text = tvNote.text!
        note.textColor = NSKeyedArchiver.archivedDataWithRootObject(tvNote.textColor!)
        note.fontName = tvNote.font?.fontName
        note.fontSize = tvNote.font?.pointSize
        note.modificationDate = NSDate()       
    }

现在稍微解释一下上面的代码:

  • noteID 属性需要 Int 类型的数字,作为 primary key。你可以创建生成任何你想要的值,只要这些值是独一无二的。在这里,我们把当前时间戳作为我们的 primary key,不过在实际的应用开发中这不是一个好主意,因为时间戳包含了太多的数字了。然而对我们目前的这个应用来说,时间戳还是一个不错的选择,毕竟这是创建独一无二数值最简单的方法。

  • 当我们第一次存储一条新笔记时,把当前时间(也就是 NSDate 对象)设置为创建日期和修改日期。

  • 这里唯一需要特殊处理的行为是将文本颜色转换成 NSData 对象,通过使用 NSKeyedArchiver 类来存储颜色对象。

接下来关注如何存储图片。我们创建一个新的方法,来处理图片数组。我们在这方法里主要做两件事情:将实际图片存储到应用的 documents 目录下,给每个图片创建 ImageDescriptor 对象,添加到 images 数组里。

先要绕点路,再次回到 Note.swift 文件中来,先看一下实现情况,然后再解释。

    func storeNoteImagesFromImageViews(imageViews: [PanningImageView]) {
        if imageViews.count > 0 {
            if images == nil {
                images = [ImageDescriptor]()
            }
            else {
                images.removeAll()
            }
            
            for i in 0..<imageViews.count {
                let imageView = imageViews[i]
                let imageName = "img_\(Int(NSDate().timeIntervalSince1970))_\(i)"
                
                images.append(ImageDescriptor(frameData: imageView.frame.toNSData(), imageName: imageName))
                
                Helper.saveImage(imageView.image!, withName: imageName)
            }
            
            imagesData = NSKeyedArchiver.archivedDataWithRootObject(images)
        }
        else {
            imagesData = NSKeyedArchiver.archivedDataWithRootObject(NSNull())
        }
    }

上面这方法里发生了哪些事情呢:

  1. 首先,我们确认 images 数组是否存在。如果为空,进行初始化,如果存在,我们只需要将里面的数据清除即可,在更新既有的笔记时,第二个方法在会非常有用。
  2. 然后对每个图片我们创建一个独一无二的名字,每个名字都类似这样:“img_12345679_1”。
  3. 使用 init 方法初始化一个新的 ImageDescriptor 方法, image view 的 frame 和名字是该方法的参数。toNSData() 方法已经实现好了,是 CGRect 的扩展,你可以从 Extensions.swift 文件里找到。目的是将 frame 转换成 NSData 对象。一旦新的 ImageDescriptor 对象准备好了,就可以添加到 images 数组里了。
  4. 我们将实际的图片存储到 documents 目录下,saveImage(_: withName:) 类方法可以在 Helper.swift 文件里找到,这里还有很多有用的类方法。
  5. 最后,当所有的 image views 都处理过后,通过 archiving(归档)我们将 images 数组转换成 NSData 对象,存储到 imagesData 属性里。上面代码中的最后一行,是 NSCoding 协议必须实现的方法。

上面的 else 看起来似乎有些多余,实际上很有用。默认情况下,imagesData 为空,如果某条笔记里没有添加图片,就会一直为空直。然而,SQLite 不识别 nil(空),SQLite 理解的是 NSNull,也就是转换成 NSData 对象。

回到 EditNoteViewController.swift 文件中,用上我们刚刚创建的方法:

func saveNote() {
    ...
    
    note.storeNoteImagesFromImageViews(imageViews)
}

现在回到 Note.swift,实现能够实际存储到数据库的方法。这里有一点比较重要的事情你需要知道:SwiftyDB 可以同步或异步执行任何数据库相关操作,选择哪种方法全看你应用的性质。然而,我建议使用异步方法,这样在进行数据库操作时,不会阻塞主线程,也不会出现 UI 控件突然冻住这种不好的用户体验。不过我还是再强调一次,选择哪种方法,完全由你决定。

在这里,我们使用异步的方式来存储数据。正如你所见,每个 SwiftyDB 方法都包含一个闭包,可以返回执行结果。你可以在这里阅读相关的信息,实际上,我建议你现在就先去阅读。

现在来实现我们的新方法,之后我们再详细说明:

    func saveNote(shouldUpdate: Bool = false, completionHandler: (success: Bool) -> Void) {
        database.asyncAddObject(self, update: shouldUpdate) { (result) -> Void in
            if let error = result.error {
                print(error)
                completionHandler(success: false)
            }
            else {
                completionHandler(success: true)
            }
        }
    }

从上面的实现方法可以知道,我们要使用相同的方法来更新笔记。我们设置 shouldUpdate 为布尔值,作为该方法的参数,然后根据 asyncDataObject 的值来觉得是否创建一个新的笔记,或者更新一个已存在的笔记。

此外,第二个参数是 completion handler。能否用合适的参数值调用它,取决于我们的存储是否成功。当你的任务在后台使用异步方法时,我建议你使用 completion handler。这样,当任务完成后,你就能够注意到调用方法了,将任何结果或者数据调回来。

上面你看到的这些,也会在其他的数据库相关方法中看到。我们会一直检查错误,然后根据是否存在结果来执行下一步的操作。在上面的例子中,如果出现了一个错误,我们就可以调用 completion handler,传入 false 值,意味着我们存储失败了,反之,我们传入 true 值,表示操作成功。

回到 EditNoteViewController 类,完成 saveNote() 方法。调用上面创建的方法,如果笔记存储成功了,pop 当前的 view controller,如果存储发生了错误,我们显示一段提示信息。

    func saveNote() {
         ...
        
        let shouldUpdate = (editedNoteID == nil) ? false : true
        
        note.saveNote(shouldUpdate) { (success) -> Void in
            dispatch_async(dispatch_get_main_queue(), { () -> Void in
                if success {
                self.navigationController?.popViewControllerAnimated(true)
                }
                else {
                    let alertController = UIAlertController(title: "NotesDB", message: "An error occurred and the note could not be saved.", preferredStyle: UIAlertControllerStyle.Alert)
                    alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: { (action) -> Void in
                        
                    }))
                    self.presentViewController(alertController, animated: true, completion: nil)
                }
            })
        }
    }
    

注意上面实现方法中的 shouldUpdate 变量,它能否得到合适的值,取决于 editedNoteID 属性是否为空,也就是笔记是否被更新了。

到现在,你可以运行 App 然后试着存储一条新笔记了。如果你是按照上面一步一步走到现在的,那么存储笔记功能已经不会出现什么差错了。

下载和列出笔记

创建和存储新笔记的功能已经实现了,我们可以继续开发从数据库从读取笔记的功能了。读取笔记意味着将笔记列在 NoteListViewController 类中,然而,在我们开始之前,先在 Note.swift 文件里读取数字。

    func loadAllNotes(completionHandler: (notes: [Note]!) -> Void) {
        database.asyncObjectsForType(Note.self) { (result) -> Void in
            if let notes = result.value {
                completionHandler(notes: notes)
            }
            
            if let error = result.error {
                print(error)
                completionHandler(notes: nil)
            }
        }
    }

SwiftyDB 里执行读取功能的方法是 asyncObjectsForType(...),是异步执行的方法。结果要么是一个错误,要么就是从数据库里读取一个 note 对象集合(数组)。在第一种情况下,我们调用 completion handler 传入 nil,告诉调用者这里在读取数据时遇到了问题。在第二种情况下,把 'Note' 对象传入 completion handler,这样可以在方法之外使用它们。

现在回到 NoteListViewController.swift 文件,首先必须声明一个数组包含 Note 对象(刚刚从数据库中读取出来)。这个数组就是 tableview 的 datasource(很明显嘛)。所以,在类的开头,加入下列代码:

var notes = [Note]()

除此之外,初始化一个新的 Note 对象,可以使用之前创建的 loadAllNotes(...) 方法:

var note = Note()

是时候写一个简单的新方法了,调用上面的方法,读取所有存储在数据库中的对象,放到 notes 数组里。

    func loadNotes() {
        note.loadAllNotes { (notes) -> Void in
            dispatch_async(dispatch_get_main_queue(), { () -> Void in
                if notes != nil {
                    self.notes = notes
                    self.sortNotes()
                    self.tblNotes.reloadData()
                }
            })
        }
    }

请注意,在读取所有的笔记后,用主线程重新加载 tableview,当然了,在重载之前,把所有的笔记存到 notes 数组里。

上面的两个方法的,就是我们所需的全部方法。有了这两个方法,我们就能从数据库里得到之前存储的笔记。别忘了 loadNotes() 必须在某个地方被调用,所以在 viewDidLoad() 方法中调用 loadNotes()

override func viewDidLoad() {
    ...
    
    loadNotes()
}

光是读取笔记还不够,一旦我们读取笔记数据,我们还要使用这些数据。让我们先更新 talbeview 的各个方法,从一共有多少行开始吧:

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return notes.count
    }

接下来让我们把笔记的数据放到 tableview 中,具体说来,我们会展示笔记的标题,创建笔记和修改笔记的日期。

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("idCellNote", forIndexPath: indexPath) as! NoteCell
        
        let currentNote = notes[indexPath.row]
        
        cell.lblTitle.text = currentNote.title!
        cell.lblCreatedDate.text = "Created: \(Helper.convertTimestampToDateString(currentNote.creationDate!))"
        cell.lblModifiedDate.text = "Modified: \(Helper.convertTimestampToDateString(currentNote.modificationDate!))"
        
        return cell
        
    }

现在运行应用吧,你创建的所有笔记都会出现在 tableview 中了。

另外一种获取数据的方法

在之前我们使用的是 asyncObjectsForType(...) 方法来下载数据库中所有的笔记,正如你所知,这个方法会返回一个数组对象(在我们的例子里,就是 Note 对象),我觉得这个方法特别有用便捷,然而不会在所有情况下都有用,某些情况下,读取实际的数值数据会更方便。

这一点 SwiftyDB 也能做到,提供另外一种方法获取数据,叫做 asyncDataForType(...) (或 dataForType(...),如果你想使用同步操作的话),它返回的是个词典类型的集合,格式 [[String: SQLiteVlalue]](在这里 SQLiteVlalue 是任何一种支持的数据类型)。

你可以在这里这里找到更多的信息,我这个任务留给你,作为一个练习,丰富 Note 类,下载简单的数据,下载数值,而不是只下载对象。

更新一条笔记

我们还想让我们的应用具有编辑既有笔记的功能,换一句话说,当用户点击某一行时,我们就呈现 EditNoteViewController 界面,出现这条笔记的所有信息,修改之后保存,我们存储的就是笔记修改后的信息了。

首先在 NoteListViewController.swift 文件里,我们需要一个新的属性来存储所选笔记的 ID,所以我们在类的顶部写入下列代码:

var idOfNoteToEdit: Int!

现在,我们实现一个 UITableViewDelegate 方法,根据所有的行找到对应的 noteID 值,通过 segue 来显示 EditViewContrller

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        idOfNoteToEdit = notes[indexPath.row].noteID as Int
        performSegueWithIdentifier("idSegueEditNote", sender: self)
    }

prepareForSegue(...) 方法里,我们把 idOfNoteToEdit 值传给接下来出现的 view controller:

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if let identifier = segue.identifier {
            if identifier == "idSegueEditNote" {
                let editNoteViewController = segue.destinationViewController as! EditNoteViewController
                editNoteViewController.delegate = self
                
                if idOfNoteToEdit != nil {
                    editNoteViewController.editedNoteID = idOfNoteToEdit
                    idOfNoteToEdit = nil
                }
            }
        }
    }

到这里我们已经完成了一半的工作了,在我们回到 EditNoteViewController 类之前,我们绕点路先去 Note 类里实现一个简单的新方法,能通过既定的 ID 值,取回单条笔记的信息,下面是实现方法:

    func loadSingleNoteWithID(id: Int, completionHandler: (note: Note!) -> Void) {
        database.asyncObjectsForType(Note.self, matchingFilter: Filter.equal("noteID", value: id)) { (result) -> Void in
            if let notes = result.value {
                let singleNote = notes[0]
                
                if singleNote.imagesData != nil {
                    singleNote.images = NSKeyedUnarchiver.unarchiveObjectWithData(singleNote.imagesData) as? [ImageDescriptor]
                }
                
                completionHandler(note: singleNote)
            }
            
            if let error = result.error {
                print(error)
                completionHandler(note: nil)
            }
        }
    }

这里有个新东西就,我们首次使用 filter 方法来对返回的结果进行限制。使用 Filter 类里的 equal(...) 方法可以设置我们想要的过滤条件。别忘了看一下这个链接,里面有更多实现过滤的方法(在从数据库里取数据或者对象时)。

通过使用上面的过滤方法,我们实际上可以让 SwiftyDB 只下载符合条件的笔记,上面方法中参数的值对应的 noteID 的笔记。当然,只会返回一条笔记,因为我们知道这里使用的是 primary key,在同样的 key 下不可能返回多个记录。

返回的结果会作为 Note 对象的数组,所以很有必要从集合里的多第一个(唯一一个)item。之后,必须将 image data(如果存在的话)转换为 ImageDescriptor 对象数组,然后将其赋值给 images 属性。这点很重要,如果跳过这一步,下载下来的笔记里的图片都无法显示出来。在最后,根据是否成功取得笔记数据,我们调用 completion handler。如果成功取得笔记,我们把读取来的对象传给 completion handler,让调用者使用,如果没有成功取得笔记,返回nil,因为没有取得对象。

现在,回到 EditNoteViewController.swift 文件,声明并初始化一个新的 Note 属性:

var editedNote = Note()

这个对象第一次用于调用上面实现的新方法,接着包含从数据库中下载下来的数据。

使用 loadSingleNote(...) 方法来,根据 editedNoteID 属性来下载特定的某条笔记。对我们而言,我们要定义 viewWillAppear(_:) 方法,在这里我们要扩展一些逻辑。

在下面的代码中你会看到,一旦通过 completion handler 获取笔记,loadSingleNotedWithID(...) 方法得到返回值,所有的值都将被赋值。也就是说,我开始设置笔记的标题、内容、文字颜色、文字字体等等,不仅如此,如果笔记里有图片,我们还会给每条笔记创建 images view 控件,控件的大小使用的当然是 ImageDescriptor 对象里具体的 frames 值。

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        
        if editedNoteID != nil {
            editedNote.loadSingleNoteWithID(editedNoteID, completionHandler: { (note) -> Void in
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    if note != nil {
                        self.txtTitle.text = note.title!
                        self.tvNote.text = note.text!
                        self.tvNote.textColor = NSKeyedUnarchiver.unarchiveObjectWithData(note.textColor!) as? UIColor
                        self.tvNote.font = UIFont(name: note.fontName!, size: note.fontSize as CGFloat)
                        
                        if let images = note.images {
                            for image in images {
                                let imageView = PanningImageView(frame: image.frameData.toCGRect())
                                imageView.image = Helper.loadNoteImageWithName(image.imageName)
                                imageView.delegate = self
                                self.tvNote.addSubview(imageView)
                                self.imageViews.append(imageView)
                                self.setExclusionPathForImageView(imageView)
                            }
                        }
                        
                        self.editedNote = note
                        
                        self.currentFontName = note.fontName!
                        self.currentFontSize = note.fontSize as CGFloat
                    }
                })
            })
        }
    }

在所有值都被赋值后,不要忘了还要把 note 赋值给 editedNote 对象,在后面我们会用上。

这里还需要最后一步:更新 saveNote() 方法,所以当一条已有笔记更新内容后,不会创建一条新的 Note 对象,不会生成一个新的 primary key 和创建日期。

所以,找到这三行代码(在 saveNote() 方法里):

let note = Note()
note.noteID = Int(NSDate().timeIntervalSince1970)
note.creationDate = NSDate()

替换成下面这堆代码:

        let note = (editedNoteID == nil) ? Note() : editedNote
        
        if editedNoteID == nil {
            note.noteID = Int(NSDate().timeIntervalSince1970)
            note.creationDate = NSDate()
        }

剩下的部分保持不变(至少现在来说是这样)。

更新笔记列表

这个时候如果测试 App,你会意识到,创建新的笔记或者编辑某条笔记后,笔记清单没有更新。这样很正常,因为你还没有开发这个功能呢,在这一章节里,我们会修复这个问题。

你可能会猜到,我们会使用 Delegation pattern 来通知 NoteListViewController 类,告知 EditViewController 里发生的变动。我们的出发点是在 EditViewController 里创建一个新的协议,协议包含两个必须实现的方法,如下:

protocol EditNoteViewControllerDelegate {
    func didCreateNewNote(noteID: Int)
    
    func didUpdateNote(noteID: Int)
}

在这两种情况下,我们都给委托方法提供新的或编辑笔记的 ID 值。现在到 EditNoteViewController 类,添加下列属性:

var delegate: EditNoteViewControllerDelegate!

最后,让我们最后一次来到 saveNote() 方法,首先找到 completion handler 闭包:

lf.navigationController?.popViewControllerAnimated(true)

将上面这行代码删掉,换成下方这堆的代码:

 if self.delegate != nil {
    if !shouldUpdate {
        self.delegate.didCreateNewNote(note.noteID as Int)
    }
    else {
        self.delegate.didUpdateNote(self.editedNoteID)
    }
}
self.navigationController?.popViewControllerAnimated(true)

从今往后,每当创建新笔记或者编辑已有笔后,对应的 delegate 方法就会被调用。但是我们只完成了一半的工作,让我们回到 NoteListViewController.swift 文件,首先在类的开头遵守新的协议:

class NoteListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, EditNoteViewControllerDelegate {
    ...
}

接下来,在 prepareForSegue(...) 方法里,让 NoteListViewController 类成为 EditNoteViewController 的委托。就在 let editNoteViewController = segue.destinationViewController as! EditNoteViewController 这行增加下方代码:

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if let identifier = segue.identifier {
            if identifier == "idSegueEditNote" {
                let editNoteViewController = segue.destinationViewController as! EditNoteViewController
                editNoteViewController.delegate = self // 增加这一行代码
                
                ...
                
            }
        }
    }

不错,大部分的工作都完成了。还没有实现两个协议方法,首先,让我们先处理创建新笔记这种情况:

    func didCreateNewNote(noteID: Int) {
        note.loadSingleNoteWithID(noteID) { (note) -> Void in
            dispatch_async(dispatch_get_main_queue(), { () -> Void in
                if note != nil {
                    self.notes.append(note)
                    self.sortNotes()
                    self.tblNotes.reloadData()
                }
            })
        }
    }

正如你所见,我们从数据库里获取 noteID 参数值对应的对象,然后(如果对象存在)我们把对象添加到 notes 数组,重新加载 tableview。

继续另外一种情况:

    func didUpdateNote(noteID: Int) {
        var indexOfEditedNote: Int!
        
        for i in 0..<notes.count {
            if notes[i].noteID == noteID {
                indexOfEditedNote = i
                break
            }
        }
        
        if indexOfEditedNote != nil {
            note.loadSingleNoteWithID(noteID, completionHandler: { (note) -> Void in
                if note != nil {
                    self.notes[indexOfEditedNote] = note
                    self.sortNotes()
                    self.tblNotes.reloadData()
                }
            })
        }
    }

在这种情况下,我们首先在 notes 词典里找到被编辑过笔记的 index,一旦之后从数据库里下载对应的笔记,用新的对象替换旧的对象,然后更新 tableview,新的修改日期就会出现了。

删除记录

还有最后一个主要的功能没有开发,就是删除笔记。很明显,我们需要在 Note 类里实现我们最后一个方法,每次想删除笔记的时候,都会调用这个方法,所以,打开 Note.swift 文件吧。

这里唯一的一个知识点就是 SwiftyDB 方法会从数据库里直接删除数据,在接下来的实现方法中你会看到这一点。和以前一样,这个操作还是异步操作,一旦执行结束,调用 completion handler,最后,有一个过滤器指明需要被删除的行。

    func deleteNote(completionHandler: (success: Bool) -> Void) {
        let filter = Filter.equal("noteID", value: noteID)
        
        database.asyncDeleteObjectsForType(Note.self, matchingFilter: filter) { (result) -> Void in
            if let deleteOK = result.value {
                completionHandler(success: deleteOK)
            }
            
            if let error = result.error {
                print(error)
                completionHandler(success: false)
            }
        }
    }

现在打开 NoteListViewController.swift,定义下一个方法 UITableViewDataSource

    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == UITableViewCellEditingStyle.Delete {
        
        }
    }

通过把上面的方法添加到代码中,每次你左滑一行笔记的时候,右边会出现默认的 Delete 按钮。而且,当用户点击 Delete 按钮时,会执行 if 后面对应的代码,如下:

    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
        if editingStyle == UITableViewCellEditingStyle.Delete {
            let noteToDelete = notes[indexPath.row]
            
            noteToDelete.deleteNote({ (success) -> Void in
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    if success {
                        self.notes.removeAtIndex(indexPath.row)
                        self.tblNotes.reloadData()
                    }
                })
            })
        }
    }

首先,找到所选中行对应的对象,然后,调用 Note 类里的新方法进行删除,如果删除成功,从 notes 数组里移除 Note 对象,重新加载 tableview,更新 UI 显示内容。

就是这么简单!

那么,关于排序呢 ?

有可能你会在想,如果对中读取出来的数据进行排序。排序非常有用,基于一个或者多个字段,执行升序或者降序排列,最后改变返回数据的顺序。例如,我们可以将我们所有的笔记按照修改日期的先后进行排序。

不行的是,在我们写这篇辅导教程时,SwiftyDB 还不支持对数据进行排序,这确实是个劣势,不过还是有解决办法的:手动排序。为了演示手动排序的方法,让我们在 NoteListViewController.swift 文件里创建最后一个方法 sortNotes()。这里我们会使用 Swift 自带的 sort() 函数:

    func sortNotes() {
        notes = notes.sort({ (note1, note2) -> Bool in
            let modificationDate1 = note1.modificationDate.timeIntervalSinceReferenceDate
            let modificationDate2 = note2.modificationDate.timeIntervalSinceReferenceDate
            
            return modificationDate1 > modificationDate2
        })
    }

由于我们无法直接比较 NSDate 对象,我们先转换成时间戳(double 类型的值)。接着执行比较,返回比较的结果。上面的代码让我们进行笔记排序,最新修改的笔记排在 notes 数组最前面。

不管什么时候,只要 notes 数组发生了改变,上面的方法就要被调用。首先,让我们更新 loadNotes 方法,如下:

func loadNotes() {
    note.loadAllNotes { (notes) -> Void in
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
            if notes != nil {
                self.notes = notes
                
                self.sortNotes()  // 添加此行代码对所有的笔记进行排序
                
                self.tblNotes.reloadData()
            }
        })
    }
}

接着,我们必须在下方两个 delegate 方法里做同样的事情:

func didCreateNewNote(noteID: Int) {
    note.loadSingleNoteWithID(noteID) { (note) -> Void in
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
            if note != nil {
                self.notes.append(note)

                self.sortNotes() // 添加此行代码对所有的笔记进行排序
                
                self.tblNotes.reloadData()
            }
        })
    }
}
func didUpdateNote(noteID: Int) {
    ...
    
    if indexOfEditedNote != nil {
        note.loadSingleNoteWithID(noteID, completionHandler: { (note) -> Void in
            if note != nil {
                self.notes[indexOfEditedNote] = note
                
                self.sortNotes()  // 添加此行代码对所有的笔记进行排序
                
                self.tblNotes.reloadData()
            }
        })
    }
}

现在再运行 App,所有的笔记都会按照它们的修改时间顺序显示。

总结

毫无疑问,SwiftyDB 是非常棒的工具,可以用在各种应用里,不费力。速度快且可靠,当我们的应用必须使用数据库的时候,SwiftyDB 可以满足各种需求。在本文的 demo 辅导教程里,我们了解了 SwiftyDB 的基本知识,还有很多东西等待你去学习。当然了,如需获得帮助,这里有官方文档供你查阅。在今天的例子讲解中,为了方便编写辅导教程,我们创建的这个数据库有一个表对应 Note 类。在实际开发中,你想创建多少表就能创建多少表,只要有对应的 model 代码即可(对应的类)。就我个人而言,我肯定会在我的项目中使用 SwiftyDB 的,实际上,我正在打算这样做。现在你已经了解了 SwiftyDB,你也见识了它如何工作的,如何实现的。SwiftyDB 能否成为你工具箱里的新成员,完全由你决定。总之,我希望阅读这篇文章并不是在浪费你的时间,希望你也学到了一些新的知识,在我们下一教程出来之前,祝您开心!

仅供参考,你可以在 GitHub 上下载完整的工程

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容

  • 作者:AppCoda,原文链接,原文日期:2016-03-16译者:Crystal Sun;校对:numbbbbb...
    梁杰_numbbbbb阅读 543评论 0 5
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,112评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,598评论 25 707
  • 方法: 可以通过 segue来传值, 可分为六步, 如下: 步骤如下: 第一步: 给containerView的s...
    木子冰洛阅读 2,973评论 0 1
  • 也许你不认识我,我也不认识你。 但是很感恩你有机会可以读到我的故事。 1、与MFC结缘与2016年年初,当时我正在...
    杨鸿_0345阅读 367评论 0 0