2

使用SwiftyDB来管理SQLite数据库

作者:GABRIEL THEODOROPOULOS,时间:2016/3/16
翻译:BigNerdCoding, 如有错误欢迎指出。原文链接

在开发应用的时候选择一种方式长久的存储数据是一件很必要的事情。这里有很多的方法可供开发者选择:创建一个文件、使用CoreData或者SQLite数据库。使用最后一种方法的话可能会有一些麻烦,因为在应用使用数据库之前,我们先要创建一个数据库、所有的数据表和字段。而且,从开发者的角度来说,对一个SQLite中的数据进行插入、更新、检索本身就是一件容易的事。

当我们使用GitHub上的一个名为SwiftyDB的新库的时候一切都变得很简单了。这个第三方库正如创建者所说的那样,是一个即插即用的插件。SwiftyDB将开发者从手动创建SQLite数据库、定义数据表和字段的工作中解脱出来。类库使用类中的属性作为数据模型自动完成了上面的工作。除此之外,所有的数据库操作都在后台完成,所以开发者可以仅仅将注意力集中在应用的逻辑实现上面。简单而强大的API接口让数据库处理变得是小菜一碟。

有必要提醒一下大家,不要期望SwiftyDB能够创造奇迹完成一些无法完成的任务。作为一个可靠的第三方库,它很好的完成了它支持的功能,但是这里还有一些特性和功能缺失也许最近或者未来会被添加进来。然而作为一个了不起的工具它依旧值得我们注意和学习,因此本篇教程我们会了解一些基本的SwiftyDB操作。

你可以在这里找到参考文档,在结束这篇教程后你应该去仔细查看一下。如果你很想在工作中使用SQLite数据库但是又犹豫不决,我相信SwiftyDB的学习会是一个很好的开端。

正如上面所说,让我们开始探索这个新的、很有前途的工具吧。

关于演示App

在我们这篇文章中我们会去创建一个简单的便签笔记应用,该应用具备一下的基本操作功能:

  • 展示便签

  • 创建新便签

  • 更新已有便签

  • 删除便签

显然,SwiftyDB会用来负责SQLite数据库中这些数据的管理。上面列举出来的所有操作能够很好的证明使用SwiftyDB开始工作能够很好的满足你的需求。

为了让大家与我保持一样的节奏,我创建了一个起始工程你需要先去将它下载下来。当下载完成后用Xcode打开简单的熟悉一下工程。正如你将看见的那样,出了与数据相关的那些功能以外其它的基本功能已经完成好了。如何你至少运行一次该项目的话那么你将会对程序的总体有个了解。

该应用是基于navigation并且在第一个视图控制器里面有一个tableview,该视图将用来展示便签列表。

clipboard.png

点击其中的某一个存在的便签我们将可以对其进行编辑和更新,并且当我们左划的时候我们可以进行删除操作:

clipboard.png

通过点击导航栏上的+号来创建一个新的便签。为了拥有足够好的演示实例,下面是编辑便签的时候我们能采用的一系列动作:

  1. 设置标题喝正文

  2. 改变字体

  3. 修改字体大小

  4. 改变文本的颜色

  5. 便签里面插入图片

  6. 移动图片并将图片放到不同的地方

上面所有操作对应的值都会保存到数据库里面。尤其是对于最后两个功能为了让大家理解更加清晰,说明如下:真实的图片被存储在应用的文件目录里面,我们保存到数据库的仅仅是图片的名称已经展示位置。不仅如此,我们还会创建一个新的类来管理这些图片(详见后文)。

clipboard.png

最后我要说一个很重要的问题我需要说明:虽然你已经下载了起始工程但是在下面一部分结束后你会有一个另一个workspace。这是因为我们将会使用CocoaPods去下载SwiftyDB库以及一些依赖的其它项。

然我们继续教程,但是首先你需要关闭Xcode里面打开的起始工程。

安装SwiftyDB

我们需要做的第一件事就是下载SwiftyDB库并在工程中使用。简单的下载库文件并添加到工程中并不会能让程序正确工作,所以我们需要使用CocoaPods来进行安装操作。操作过程很简单,即使以前你没有使用过CocoaPods这也不会需要花什么时间。当然如果没有使用过可以点击前面的链接查看参考文档。

安装CocoaPods

我们将会在系统中安装CocoaPods,当然如果你以前就安装过的话可以跳过这一步。如果没有则继续阅读并打开终端。输入下下面命令来安装CocoaPods:

sudo gem install cocoapods

按下回车键,输入你的密码然后听首歌(给我一首歌的时间?)等待下载进程的完成。完成后不要急着关闭终端,后面依旧需要使用到。

安装SwiftyDB以及其它依赖

在终端里面使用cd命令切换到起始工程所在的目录:

cd  PATH_TO_THE_STARTER_PROJECT_DIRECTORY

是时候创建描述我们需要通过CocoaPods下载的类库文件Podfile了。最简单的创建方式就是输入以下命令:

pod init

输入命令后工程文件目录下面会有一个名为Podfile的新文件。使用一个文本编辑器打开该文件(最后不要使用TextEdit),并输入下面的代码:

use_frameworks!

target 'NotesDB' do
    pod "SwiftyDB"
end

clipboard.png

这正完成任务的代码部分是pod "SwiftyDB"。该行命令会让CocoaPods为你下载安装好SwiftyDB以及所有的依赖性,并且会在工程目录下面创建一个新的目录和Xcode workspace。

一旦你完成了该文件的编辑,保存并关闭编辑器。然后确保你也关闭了起始工程返回终端,输入以下命令:

pod install

clipboard.png

静静地等上一会,然后一切就都准备好了。这次我们不打开起始工程文件,而是去打开NotesDB.xcworkspace文件。

开始使用SwiftyDB - Our Model

NotesDB工程里面有一个名为Note.swift的文件,当前该文件里面还是空的。该文件就是今天我们的切入点,在文件里面我们会创建一系列代表便签的程序实体的类。在理论层面上说,也就是创建MVC模式中的Model。

我们首先要做的是导入SwiftyDB类库,正如你猜想的那样我们在文件的最上面加入以下代码:

import SwiftyDB

现在我们来定义工程中最重要的类:

class Note: NSObject,Storable {

}

当你在工作中使用SwiftyDB的时候有一些具体的规则需要你遵守,在上面的代码部分你能看见其中的两个规则:

  1. 使用SwiftyDB将带有属性的类存储到数据库的时候意味着该类必需是NSObeject的一个子类。

  2. 使用SwiftyDB将带有属性的类存储到数据库的时候意味着该类必需遵循Storable协议(该协议是SwiftyDB中的)。

接下来我们需要思考该类里面应该定义哪些属性,这里又有一个SwiftyDB中的规则:为了在检索数据库时候能够加载整个Note对象,属性的数据类型必须存在于该列表中,而不是使用字典数组的简单数据(array with dictionaries)。如果你属性的类型与支持的类型不兼容,那么你需要采取一些其它的办法将它转化为支持的类型(具体的操作细节在后面介绍)。不兼容类型的属性默认情况下在保存到数据库的时候会被忽略,并且数据库表中也不会创建对应的字段。并且,如果对于某些属性你不想保存到数据库的时候我们也有解决方法。

遵循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!
}

注意到该类中farme被定义为了一个NSData对象而不是CGRect。为了后面能更容易的保存该数据到数据库中,这么做是很有必要的。你等会就能看见是如何处理的已经明白为什么我们遵循了NSCoding协议。

回到Note,我们定义一个如下的ImageDescriptor图像数组:

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

现在正好提出来这里的一个局限性,那就是SwiftyDB不会存储数据集合到数据库里面。简单来说就是我们的图片数组永远都不会存储到数据库李敏啊,所以我们需要明白如何处理这种情况。一个可能的办法就是使用数据库支持的类型(查看该部分的开始的链接了解详情)来完成存储操作,其中最适合的数据类型就是NSData。所以我们使用下面的新类型来替换图片数组完成保存操作:

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

但是如何将ImageDescriptor对象的images数组转化为NSData类型的imagesData对象呢?解决方案是使用NSKeyedArchiver类来对images数组进行归档并生成一个NSData对象。我们后面看见代码部分是怎么实现的,但是现在我们知道了需要做些什么。我们回到ImageDescriptor类里面做些补充。

如你所知,当且仅当类中的所有属性能够被序列化的时候该类才能被归档(在其他的语言里面也被称为序列化)。在我们的程序里面ImageDescriptor里面的两个属性的数据类型是NSDataString,所以是可序列化。然而这还不够,为了能够成功的完成归档和解档操作我们需要分别需要进行编码和解码,这也就是为什么会需要NSCoding协议。使用该协议实现下面的方法(其中一个是init方法),并且我们对两个属性进行编码和解码操作:

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中有些错误提示的话,请先至少编译一次工程。

设置主键和需要忽略的属性

在处理数据库的时候我们总是建议使用主键,因为主键能够让你唯一标识一条记录并且通过它来执行某些操作(更新某一条记录)。你可以在点我找到关于主键的定义。

在SwiftyDB中数据表对应的类里面的一个活或者多个属性定义的主键其实很简单。该类库提供了PrimaryKeys协议,该协议应该被所有拥有主键的数据表对应的类实现,这样就能唯一标识表中的记录对象了。实现的方法很直接也很标准,让我们直接进入主题:

NoteDB工程里面,你可以发现一个名为Extensions.swift的文件。在导航栏点击该文件并打开它。加入以下几行代码:

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

在Demo中,我们希望noteID属性作为sqlite数据库对应数据表的唯一键值。然而,如果需要多个主键的话,你可以字使用逗号进行分割(例如,return ["key1", "key2", "key3"])。

除此之外,一个类中并不是所有属性都需要存储到数据库里面,你需要明确表明以便SwiftyDB不会将其保存。例如,在Note中有两个属性不需要保存到数据库中(一个是无法保存一个是我们不希望保存):images数组和database对象。我们如何明确排除这两个呢?遵循SwiftyDB的另一个协议IgnoredProperties并进行实现:

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类最低限度的实现之后,是时候回到我们程序功能性的实现了。到目前为止我们还没有在新建的类里面添加任何方法;接下来我们一步步来实现那些缺失的功能。

首先我们需要拥有notes,因此我们必须让应用知道如何使用SwiftyDB和两个新建的类来保存新建的notes。该功能大部分都发生在EditNoteViewController里面,所以我们在导航栏里面找到对应文件并打开。在我们写下第一行到吗之前,我认为突出以下文件里面的一些属性是很重要的:

  • imageViews:该数组包含了所有添加到某一个便签里面的图片对象。不要忘记该数组的存在;后面他回很有用处。

  • currentFontName:该属性记录了当前textview中的所使用的字体名称。

  • currentFontSize:该属性记录了当前textview中的所使用的字体大小。

  • editedNoteID:该属性就是note的主键值noteID。我们在后面会使用到。

因为起始工程里面大部分的功能都已经存在了,我们所需要的是实现那些缺失的逻辑方法saveNote()。该功能需要做两件事:首先我们不会保存那些没有标题或者正文的便签。其次,当保存便签的时候键盘存在的话需要进行隐藏:

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

后面我们继续实例化一个Note对象,并进行正确的赋值到对象的属性中。图片需要特殊处理,我们在后面给出方法。

 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属性需要一个整数作为数据库中的主键。你可以自己创建或者自动生成一个数字,只要能够保证它的值是唯一的就行了。在这里我们使用当前时间戳的整数部分作为记录的主键。但是在真实世界的应用里面这并不是一个很好的注意,因为时间戳含有太多的数字了。不过在演示应用里面不会有什么问题,因为它能已简单的方法生成一个唯一的主键值。

  • 当第一次我们我们保存便签的时候创建时间和修改时间都被赋值为当前时间了。

  • 唯一需要特别注意的是我们将textview中文本的颜色转化为了一个NSData对象。该对象使用了NSKeyedArchiver类对颜色进行归档。

我们现在来看看如何保存图片。我们将创建一个新的方法来保存图片数组。在里面我们做两件事:我们将每一个真实的图片保存到应用的文件目录里面,并且为每一个图片创建一个ImageDescriptor对象。每一个ImageDescriptor对象都会被添加到images数组里面。

为了创建这个方法我们需要绕点弯,再次回到Note.swift文件中。我们先看一下代码,后米在讲解:

func storeNoteImagesFromImageViews(imageViews: [PanningImageView]) {
    if imageViews.count > 0 {
        if images == nil {
            images = ImageDescriptor
        }
        else {
            images.removeAll()
        }
        for i in 0..&lt;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. 然后我们为每一个image view的图片创建一个唯一的名称。名称类似于:“img_12345679_1”。

  3. 使用我们自定义的init方法初始化一个新的ImageDescriptor对象并将image view frame和image name作为参数传递过去。toNSData()方法是CGRect类拓展的一个方法,你能在Extensions.swift中找到该函数。该函数的目的是将一个frame转化为NSData对象。当新的ImageDescriptor对象一切就绪的时候,我们将它添加到images数组中。

  4. 我们将真实的图片保存到了文件目录中。saveImage(_: withName:)类方法能够在Helper.swift文件中找到,该类里面有很多的有用函数。

  5. 最后,当image views的处理都完成后,我们通过归档将images数组转化为一个NSData对象,并赋值给imagesData属性。最后一行代码就是为什么在ImageDescriptor需要遵循NSCoding协议并实行其方法。

看上去上面的else部分是多余的,其实不然。默认情况下imagesData属性是nil的,并且如果没有添加图像的话,它依然应该是nil的。然而,“nil”并不会被SQLite所识别。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的参数设置为false并调用,这意味着我们保存操作失败了。相反,我们传递true来标识操作成功。

再一次我们回到EditNoteViewController类,我们继来完成saveNote()函数。我们立刻调用创建函数,并且如果保存成功的话我们会弹出当前的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属性是不是空值,这意味着如果便签存在的话就执行更新操作否则执行新建。

到此你可以运行程序并且新建便签了。如果你是按照教程一步步来的话新建保存操作不会有什么问题。

加载并展示便签列表

伴随着新建和保存便签操作的完成,我们现在将注意力转移到从数据库中加载保存的便签。加载便签的操作是在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(...),并且是以异步方式工作的。返回的结果要么是错误信息,要么就是从数据库中加载出来的便签对象的集合(一个数组)。第一种情况下,我们将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.tblNotes.reloadData()
            }
        })
    }
}

注意到当所以的数据都获取到的时候,我们使用主线程重新加载了tableview。当然在此之前需要持有notes数组。

上面的两个方法就是我们在从数据库加载数据所需的全部。简单吧!不要忘了在viewDidLoad函数里面调用loadNotes()

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

仅仅是加载出数据还不够,在加载后我们还需要最起码使用一次。所以开始修改tableview的方法,先从返回的行数处开始:

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中展示出来。

另一种获取数据的方法

在前面我们使用了SwiftyDB中的asyncObjectsForType(...)函数加载数据库中的便签数据。正如你所见,该方法会返回一个对象数组(在这里是Note对象),并且我认为该方法很方便。然而该方法在从数据库检索对象的时候并不总是有用;这里还有其它更方便的方法从真实数据值总获取一个数据数组。

SwiftyDB提供了另外的获取数据的方法能够帮助到你。函数的名称是asyncDataForType(...)(如果你想同步操作的话可以调用函数dataForType(...)),该函数会返回一个[[String: SQLiteValue]]格式的字典(SQLiteValue是任何被支持的数据类型)。

你可以在点我点我找到更多的说明。我将这部分留给读者作为丰富Note类的一个练习并且加载简单的数据,而不仅仅是加载上面的对象。

更新一个Note

我们的Demo应该具备对已有便签的编辑和更新的功能。换句话说,就是当用户点击选择某一个cell的时候,EditNoteViewController需要呈现出对象便签的细节,并且再次保存的时候需要更新修改时间到数据库里面。

我们从NoteListViewController.swift文件开始,我们需要定义一个新的属性来标识当前选择的便签ID,所以添加如下代码:

var idOfNoteToEdit: Int!

现在我们实现下一个UITableViewDelegate函数,该函数中我们基于选择的行来获取noteID的值,然后执行转场segue操作到EditNoteViewController

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

prepareForSegue(...)函数中我们将idOfNoteToEdit的值传递到下一个视图控制器:

overide func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {    
    if let identifier = segue.identifier {
        if identifier == "idSegueEditNote" {
            let editNoteViewController = segue.destinationViewController as! EditNoteViewController
            
            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(...)方法来设置我们想要的筛选限制条件。不要忘记点击该链接去了解使用filters从数据库中筛选出我们想要的数据和对象。

通过上面演示的那样使用filter,我们让SwiftyDB去数据库中加载noteID与我们作为参数设置的值相同的记录。当然,因为我们使用的字段是主键所以只会有一个数据被检索到,数据库中书不可能存在多条主键相同的记录的。

检索到的数据以Note数组的形式返回给我们了,所以我们获取该数组中的第一个对象。之后,我们肯定需要将图像数据(如果存在的话)转化为一个ImageDescriptor对象的数组,并赋值给image属性。这非常的重要,因为如果我们跳过了该步骤图像就不会被添加到note中也就无法显示出来了。

最后,我们依据note是否检索成功来调用completion handler。第一种情况,我们将获取的对象数据传递给completion handler,这样调用者就能使用该数据了,第二种情况下犹豫没有检索到我们传递了nil。

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

var editedNote = Note()

该对象第一次使用就是用于调用上面实现新函数,然后保存数据库中加载的数据。

我们通过将editedNoteID作为条件调用loadSingleNoteWithID(...)方法实现加载数据。为了实现这个目的,我们定义viewWillAppear(_:)函数,并且进行逻辑拓展。

正如你将在下面代码片段中看见的那样,loadSingleNoteWithID(...)函数通过completion handler返回检索结果的适合,所有的属性的值都会被正确设置。这意外着我们设置了便签标题、正文、文本颜色、字体等等,但还不止这些。如果便签里面还有图片的话,我们需要使用ImageDescriptor对象中的frame为每个图片创建一个image view。

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对象,并以新的创建时间和主键插入的数据库中。

所以,找到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()
}

其余的保持不变,最起码现在不需要修改。

更新Notes List

如果你现在测试应用的话,你就会发现等你创建了一个新的便签或者更新完当前已久存在的便签的时候notes list并不会同步更新。之所以会这样是应用还没有实现这个功能,在文章的这部分我们将会解决这个不该存在的问题。

正如你可能猜想到的一样,我门会使用代理模式(Delegation pattern)去通知NoteListViewController类关于EditNoteViewController中对于便签的任何更改。我们先从在EditNoteViewController中创建一个新的协议开始,该协议需要两个函数,如下所示:

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

两种情况下我们都为代理方法提供了新建时或者更新编辑时的ID值。现在我们去EditNoteViewController类里面添加如下的属性:

var delegate: EditNoteViewControllerDelegate!

最后我们再次查看最新版本的saveNote()函数。首先找到completion handler block中的下面这行代码:

self.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)

无论什么时候创建一些新的便签获取更新一个已经存在的便签,都会调用正确的委托函数。但是我们仅仅做了一半的工作。现在回到NoteListViewController.swift文件,首先我们需要在类的头部遵循新的协议:

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

接下来,在prepareForSegue(...)函数里面让该类作为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  // Add this line.
            
            ...
        }
    }
}

干的漂亮,大部分的工作都已经完成了。我们还没有完成的工作就是两个委托方法的实现。首先,我们来处理新建便签:

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.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.tblNotes.reloadData()
            }
        })
    }
}

我们首先找到需要更新便签的在notes数组中的索引号。当事件发生的时候,我门从数据库中加载出最新的便签并将原有的对象替换成最新的。通过刷新tableview,最近更新便签的时间将会被正确更新。

删除记录

最后一个Demo中缺少的主要功能就是便签的删除。很容易就能明白最后一个Note类中需要实现的方法就是,每次删除便签时候都需要调用的方法,所以再次打开Note.swift文件。

正如你即将看见的实现,这里唯一的新东西就是SwiftyDB中执行真实数据库删除操作的方法。和前面一样,这里依旧采用异步操作,并且当执行完成后我们好要调用completion handler。最后,这里有一个filter指定数据库中需要删除记录的行数。

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 {
        
    }
}

在我们的代码中添加了上面的函数后,当我门每次向左滑动cell的时候,默认的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()
                }
            })
        })
    }
}

首先,我门在notes集合里面找到与选中cell对应的note对象。然后,调用Note类中的新方法对其进行删除,并且如果删除操作成功的话我们将其从notes数组中移除并重载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
    })
}

因为NSData对象无法直接进行比较,我门首先将它转化为时间戳。然后在进行比较并返回结果。上面的代码会让最近修改的便签位于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()  // Add this line to sort notes.
                
                self.tblNotes.reloadData()
            }
        })
    }
}

然后在下面两个委托函数也要进行调用:

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() // Add this line to sort notes.
                
                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()  // Add this line to sort notes.
                
                self.tblNotes.reloadData()
            }
        })
    }
}

再次运行Demo,你会发现tableview中的便签是基于时间进行排序的。

总结

毫无疑问,SwiftyDB是一个非常棒的工具,在很多的应用中毫不费力就能使用。对于其支持的操作它速度很快且很可靠,并且在我们app中需要使用数据库的时候它很好的满足了需求。在这个Demo教程里面我们了解了该类库的一些基本概念和操作,但是这也是你必须要知道的。当然,从官方的文档里面你能找到更多的帮助和指引。在今天的示例中,由于是一篇教程,我门只创建了一个与Note类对应的数据库。在真实世界的应用中,你可以创建任意多的数据库只要你想,只要你在代码中创建了相应的model(这里就是对应的类)。个人而言,我肯定会在我的工程里面使用SwiftyDB,事实上我已经打算这么干了。在任何情况下,你现在已经知道了它是如何工作的以及如何进行调用操作。能不能在你的工具箱里面再加上这个完全取决于你自己。不论怎样,我希望你阅读这篇文章的时间没有被浪费,并且你学到了一些新的知识或者更低。在下一篇教程到来之前一切如意吧!

作为参考,你可以在这里下载到完整的工程。


BigNerdCoding
1.2k 声望125 粉丝

个人寄语: