用 SwiftyDB 管理 SQLite 数据库
选择哪种数据持久化的方式,是我们在开发 App 时常常遇到的问题。我们有太多选择了:创建一个单独的文件、使用 CoreData 或者创建 SQLite 数据库。使用 SQLite 数据库有点麻烦,因为首先要先创建数据库,提前写好表和字段。此外,从编程的角度来看,数据的存储、更新、和获取都不是很容易的操作。 而当我们使用 GitHub 上的 SwiftyDB 这个第三方库时,上面的这些问题都可以轻而易举地解决。SwiftyDB,用作者的话来说,就是即插即用型的好帮手。SwiftyDB 将开发者从繁重的手动创建 SQLite 数据库的工作中解放出来,再也不用提前定义好各种表和字段了。SwiftyDB 中类的属性能够自动完成上述工作,可以直接用类作为数据模型。除此之外,所有对数据库的操作都被封装起来,开发者可以把所有的注意力放到应用的逻辑层面上。简单强悍的 API 可以让处理数据成为小菜一碟的事情。 不过需要强调一下,SwiftyDB 并不能创造奇迹。它只是一个靠谱的第三方库,可以很好地完成它该做的事情(虽然有一些特性目前还不具备)。尽管如此,它仍然是一个非常好用的工具,值得你花时间学习。在本篇文章中,我们将学习 SwiftyDB 的基本使用操作。 可以从这里找到文档,看完这篇文章后最好再去看看文档。如果你一直想用 SQLite,可是从来没有真正开始,那 SwiftyDB 是一个好的开始。 好了,让我们开始探索这个全新的、令人期待的工具吧。 关于 Demo App在这篇文章中,我们要创建一个非常简单的笔记应用,可以实现如下这些基本操作:
很明显,SwiftyDB 将要管理一个 SQLite 数据库,上面列出的操作足以向你展示如何使用 SwiftyDB。 简单起见,我事先创建了一个工程,点击下载然后打开工程。用 Xcode 打开工程后,能够看到所有的基本功能,不过缺少与数据有关的代码。运行项目,你就能看到全貌了。 应用有一个导航栏,在第一个 view controller 中,有一个 tableview 列出所有笔记。 <center> 点击某个笔记,我们可以编辑更新内容,如果向左滑动某条笔记,可以删除笔记: <center> 创建一个新笔记只需点击导航栏上的加号按钮,下面是我们在编辑笔记时可以进行的操作:
上述所有值的改变都会存储到数据库中。在最后两条中,图片实际上是存储在应用的 documents directory 中,我们在数据库中只是存储图片的名字和 frame。此外,我们还要创建一个类来管理图片(更多细节参见后面的内容)。 <center> 最后还要强调一点,虽然你只是下载了一个简单的项目,但是在下一节中它会变成一个 workspace,因为我们要使用 CocoaPods 来下载 SwiftyDB 以及其他依赖项目。 准备好了吗?如果你在 Xcode 中打开了刚刚下载的初始工程,那么请先关闭。 安装 SwiftyDB第一件事情就是下载 SwiftyDB,然后在工程中使用。下载库的文件然后放到工程中可不管用,我们要先安装 CocoaPods。安装过程不复杂,不会花费太多时间,即使你从来没有用过 CocoaPods。详细内容请点击链接。 安装 CocoaPods我们要将 CocoaPods 安装到系统中,如果你已经安装了 CocoaPods,那么请跳过这一步,如果没有,那么打开 Terminal 终端 ,输入下列命令: sudo gem install cocoapods 然后按回车,输入 Mac 密码,等一会然后开始下载,下载完毕后不要关闭 Terminal 终端 ,我们之后还会用到。 安装 SwiftyDB 和其他的依赖库使用 cd 命令找到初始工程对应的文件夹(仍然是在 Terminal 终端中进行操作)。 cd PATH_TO_THE_STARTER_PROJECT_DIRECTORY 现在可以创建 Podfile 文件了,我们在 Podfile 里写出我们需要的下载的库。最简单的方法是输入下列命名,让 CocoaPods 给我们创建一个 Podfile。 pod init 一个名为 use_frameworks! target 'NotesDB' do pod "SwiftyDB" end <center> 这行代码实际上就做了 编辑完 pod install <center> 安装完毕之后继续。我们这次不再打开初始工程,而是打开 <center> 开始使用 SwiftyDB - 我们的 Model在 首先需要引入 SwiftyDB 库,在文件的头部输入如下代码: import SwiftyDB 现在,声明最重要的一个类: class Note: NSObject,Storable { } 我们使用 SwiftyDB 时,需要遵循几条规则,上面这个类的第一行体现出其中两条:
现在,我们要想一想,这个类需要哪些属性,这就需要了解 SwiftyDB 的一条新规则:从数据库中获取数据时, 目前需要说明的最后一条要求:遵守 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! ... } 除了第一个之外,其他的无需多言。对象初始化后(如果数据库不存在)会创建一个新的数据库(名为 你可能会注意到,上面的属性都是描述一条笔记和我们想存储的特性(标题、问题、文字颜色、字体和大小、创建和修改日期),但是唯独没有笔记中存储的图片。哈哈,我是故意的,我要给图片单独创建一个类,只存储两个属性:图片的名字和尺寸。 所以,继续在 class ImageDescriptor: NSObject,NSCoding { var frameData: NSData! var imageName: String! } 注意,在类中,图片的 frame 是一个 回到 class Note: NSObject,Storable { ... var images: [ImageDescriptor]! ... } 这里有一个限制,现在是时候提到它了,就是实际上 SwiftyDB class Note: NSObject,Storable { ... var imageData:NSData! ... } 但是我们如何才能将带有 如你所知,一个类可以被归档(在其他编程语言中也就做 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") } } 更多关于 除此之外,我们定义一个便利的自定义的 class ImageDescriptor: NSObject,NSCoding { ... init(frameData: NSData!,imageName: String!) { super.init() self.frameData = frameData self.imageName = imageName } } 在这一节中我们快速介绍了 SwiftyDB 库。虽然我们还没有大量使用 SwiftyDB,但是这部分很重要,因为它包含三个要点:
主键和忽略属性在和数据库打交道时,强烈推荐使用 在 SwiftyDB 数据库中,将类中的某个或某些属性定义为主键的操作非常简单,库里提供了 在 extension Note: PrimaryKeys { class func primaryKeys() -> Set<String> { return ["noteID"] } } 在我们的 demo 里,我想让 除此之外,并不是类中所有的属性都要存储到数据库中,你应该明确指出哪些不存储。例如,在 extension Note: IgnoredProperties { class func ignoredProperties() -> Set<String> { return ["images","database"] } } 如果还有更多属性我们不想存储到数据库中,那么也需要添加到上面的代码中,例如,假设我们有这么一个属性: var noteAuthor: String! 我们不想把它存储到数据库中,这就需要把这个属性添加到 extension Note: IgnoredProperties { class func ignoredProperties() -> Set<String> { return ["images","database","noteAuthor"] } } 保存一个新笔记我们在 首先要有笔记,需要告诉 App 如何正确地使用 SwiftyDB 来保存笔记和两个新创建的类。大部分的操作会在
基础的功能已经在初始工程中提前写好了,我们需要做的就是补全缺失的 func saveNote() { if txtTitle.text?.characters.count == 0 || tvNote.text.characters.count == 0 { return } if tvNote.isFirstResponder() { tvNote.resignFirstResponder() } } 继续初始化一个新的 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() } 现在稍微解释一下上面的代码:
接下来看如何存储图片。我们创建一个新的方法来处理图片数组。这个方法主要做两件事:将实际图片存储到应用的 documents 目录下,给每个图片创建 在实现这个方法之前,我们先要修改一下 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()) } } 上面这个方法到底做了什么呢:
上面的 回到 func saveNote() { ... note.storeNoteImagesFromImageViews(imageViews) } 现在回到 这里我们用异步方式来存储数据。如你所见,每个 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) } } } 从上面的实现方法可以知道,我们要使用相同的方法来更新笔记。把 此外,第二个参数是 completion handler。能否用合适的参数值调用它,取决于我们的存储是否成功。当你的任务在后台使用异步方法时,我建议你使用 completion handler。这样,当任务完成后,你就能通知调用方法,将任何结果或者数据调回来。 上面你看到的这些,其他的数据库相关方法中也有。我们会先检查错误,然后根据是否存在结果来执行下一步的操作。在上面的例子中,如果出现错误,我们就可以调用 completion handler,传入 回到 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) } }) } } 注意上面方法中的 现在,你可以运行 App 然后试着存储一条新笔记了。如果你是按照上面一步一步走到现在的,那么存储笔记功能已经可以正常使用了。 下载和列出笔记创建和存储新笔记的功能已经实现了,我们可以继续开发读取笔记功能了。读取笔记意味着将笔记列在 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 里执行读取功能的方法是 现在回到 var notes = [Note]() 除此之外,初始化一个新的 var note = Note() 是时候写一个简单的新方法了,调用上面的方法,读取所有存储在数据库中的对象,放到 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.当然,在重载之前,把所有的笔记存到 上面的两个方法就是我们所需的全部方法。有了这两个方法,我们就能从数据库里得到之前存储的笔记。别忘了, 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 也能做到,它提供了另外一种方法来获取数据: 你可以在这里和这里找到更多的信息,我把这个任务留给你,作为一个练习:修改 更新一条笔记我们还想让应用具有编辑笔记的功能,换句话说,当用户点击某一行时,我们就显示 首先,在 var idOfNoteToEdit: Int! 下面我们来实现一个 func tableView(tableView: UITableView,didSelectRowAtIndexPath indexPath: NSIndexPath) { idOfNoteToEdit = notes[indexPath.row].noteID as Int performSegueWithIdentifier("idSegueEditNote",sender: self) } 在 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 } } } } 到这里我们已经完成了一半的工作了,在我们回到 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) } } } 这里有个新东西,我们首次使用 通过上面的过滤方法,我们实际上可以让 SwiftyDB 只加载符合条件的笔记:上面方法中参数的值对应的 返回的结果会作为 现在,回到 var editedNote = Note() 这个对象首先调用上面实现的新方法,然后存储从数据库中加载的数据。 使用 在下面的代码中你会看到, 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 } }) }) } } 在所有属性都被赋值后,不要忘了把 这里还需要最后一步:更新 所以,找到这三行代码(在 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,你会发现创建新的笔记或者编辑某条笔记后,笔记清单没有更新。这很正常,因为你还没有开发这个功能呢,在这一节中,我们会修复这个问题。 你可能已经猜到了,我们会使用 protocol EditNoteViewControllerDelegate { func didCreateNewNote(noteID: Int) func didUpdateNote(noteID: Int) } 在这两种情况下,我们都给委托方法提供新的或编辑笔记的 ID 值。现在到 var delegate: EditNoteViewControllerDelegate! 最后,我们最后一次修改 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 方法就会被调用。目前我们只完成了一半的工作,让我们回到 class NoteListViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,EditNoteViewControllerDelegate { ... } 接下来,在 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() } }) } } 如你所见,我们从数据库里获取 继续实现另一个操作: 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() } }) } } 在这种情况下,我们首先在 删除记录还有最后一个主要的功能没有开发,那就是删除笔记。很明显,我们需要在 这里唯一的一个知识点就是 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) } } } 现在打开 func tableView(tableView: UITableView,commitEditingStyle editingStyle: UITableViewCellEditingStyle,forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == UITableViewCellEditingStyle.Delete { } } 把上面的方法添加到代码中之后,每次你左滑一行笔记,右边会出现默认的 func tableView(tableView: UITableView,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() } }) }) } } 首先,找到所选中行对应的对象,然后,调用 就是这么简单! 那么,如何排序呢?你可能正在想,如何对读取出来的数据进行排序。排序非常有用,可以基于一个或者多个字段进行升序或降序排列,最后改变返回数据的顺序。例如,我们可以将我们所有的笔记按照修改日期的先后进行排序。 不幸的是,在我写这篇教程时,SwiftyDB 还不支持对数据进行排序,这确实是个劣势,不过还有一个解决办法:手动排序。为了演示手动排序的方法,我们在 func sortNotes() { notes = notes.sort({ (note1,note2) -> Bool in let modificationDate1 = note1.modificationDate.timeIntervalSinceReferenceDate let modificationDate2 = note2.modificationDate.timeIntervalSinceReferenceDate return modificationDate1 > modificationDate2 }) } 由于我们无法直接比较 只要 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 的基本知识,还有很多东西等待你去学习。当然,如需更多帮助,这里有官方文档供你查阅。在今天的例子讲解中,为了方便编写辅导教程,我们创建的这个数据库有一个表对应 仅供参考,你可以在 GitHub 上下载完整的工程
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |