使用SwiftyDB来管理SQLite数据库

使用SwiftyDB来管理SQLite数据库git

做者:GABRIEL THEODOROPOULOS,时间:2016/3/16
翻译:BigNerdCoding, 若有错误欢迎指出。原文连接github

在开发应用的时候选择一种方式长久的存储数据是一件很必要的事情。这里有不少的方法可供开发者选择:建立一个文件、使用CoreData或者SQLite数据库。使用最后一种方法的话可能会有一些麻烦,由于在应用使用数据库以前,咱们先要建立一个数据库、全部的数据表和字段。并且,从开发者的角度来讲,对一个SQLite中的数据进行插入、更新、检索自己就是一件容易的事。sql

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

有必要提醒一下你们,不要指望SwiftyDB可以创造奇迹完成一些没法完成的任务。做为一个可靠的第三方库,它很好的完成了它支持的功能,可是这里还有一些特性和功能缺失也许最近或者将来会被添加进来。然而做为一个了不得的工具它依旧值得咱们注意和学习,所以本篇教程咱们会了解一些基本的SwiftyDB操做。swift

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

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

关于演示App

在咱们这篇文章中咱们会去建立一个简单的便签笔记应用,该应用具有一下的基本操做功能: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,事实上我已经打算这么干了。在任何状况下,你如今已经知道了它是如何工做的以及如何进行调用操做。能不能在你的工具箱里面再加上这个彻底取决于你本身。不论怎样,我但愿你阅读这篇文章的时间没有被浪费,而且你学到了一些新的知识或者更低。在下一篇教程到来以前一切如意吧!

做为参考,你能够在这里下载到完整的工程。

相关文章
相关标签/搜索