作者:AppCoda,原文鏈接,原文日期:2016-03-16
譯者:Crystal Sun;校對:numbbbbb;定稿:shanks
選擇哪種數(shù)據(jù)持久化的方式,是我們在開發(fā) App 時常常遇到的問題。我們有太多選擇了:創(chuàng)建一個單獨的文件、使用 CoreData 或者創(chuàng)建 SQLite 數(shù)據(jù)庫。使用 SQLite 數(shù)據(jù)庫有點麻煩,因為首先要先創(chuàng)建數(shù)據(jù)庫,提前寫好表和字段。此外,從編程的角度來看,數(shù)據(jù)的存儲、更新、和獲取都不是很容易的操作。
而當我們使用 GitHub 上的 SwiftyDB 這個第三方庫時,上面的這些問題都可以輕而易舉地解決。SwiftyDB,用作者的話來說,就是即插即用型的好幫手。SwiftyDB 將開發(fā)者從繁重的手動創(chuàng)建 SQLite 數(shù)據(jù)庫的工作中解放出來,再也不用提前定義好各種表和字段了。SwiftyDB 中類的屬性能夠自動完成上述工作,可以直接用類作為數(shù)據(jù)模型。除此之外,所有對數(shù)據(jù)庫的操作都被封裝起來,開發(fā)者可以把所有的注意力放到應(yīng)用的邏輯層面上。簡單強悍的 API 可以讓處理數(shù)據(jù)成為小菜一碟的事情。
不過需要強調(diào)一下,SwiftyDB 并不能創(chuàng)造奇跡。它只是一個靠譜的第三方庫,可以很好地完成它該做的事情(雖然有一些特性目前還不具備)。盡管如此,它仍然是一個非常好用的工具,值得你花時間學習。在本篇文章中,我們將學習 SwiftyDB 的基本使用操作。
可以從這里找到文檔,看完這篇文章后最好再去看看文檔。如果你一直想用 SQLite,可是從來沒有真正開始,那 SwiftyDB 是一個好的開始。
好了,讓我們開始探索這個全新的、令人期待的工具吧。
關(guān)于 Demo App
在這篇文章中,我們要創(chuàng)建一個非常簡單的筆記應(yīng)用,可以實現(xiàn)如下這些基本操作:
- 列出筆記
- 創(chuàng)建新的筆記
- 更新已經(jīng)創(chuàng)建的筆記的內(nèi)容
- 刪除筆記
很明顯,SwiftyDB 將要管理一個 SQLite 數(shù)據(jù)庫,上面列出的操作足以向你展示如何使用 SwiftyDB。
簡單起見,我事先創(chuàng)建了一個工程,點擊下載然后打開工程。用 Xcode 打開工程后,能夠看到所有的基本功能,不過缺少與數(shù)據(jù)有關(guān)的代碼。運行項目,你就能看到全貌了。
應(yīng)用有一個導航欄,在第一個 view controller 中,有一個 tableview 列出所有筆記。
<center>

</center>
點擊某個筆記,我們可以編輯更新內(nèi)容,如果向左滑動某條筆記,可以刪除筆記:
<center>

</center>
創(chuàng)建一個新筆記只需點擊導航欄上的加號按鈕,下面是我們在編輯筆記時可以進行的操作:
- 設(shè)置筆記的標題和內(nèi)容。
- 更改字體。
- 更改字體的大小。
- 更改字體的顏色。
- 添加圖片。
- 移動圖片到另外一個位置。
上述所有值的改變都會存儲到數(shù)據(jù)庫中。在最后兩條中,圖片實際上是存儲在應(yīng)用的 documents directory 中,我們在數(shù)據(jù)庫中只是存儲圖片的名字和 frame。此外,我們還要創(chuàng)建一個類來管理圖片(更多細節(jié)參見后面的內(nèi)容)。
<center>

</center>
最后還要強調(diào)一點,雖然你只是下載了一個簡單的項目,但是在下一節(jié)中它會變成一個 workspace,因為我們要使用 CocoaPods 來下載 SwiftyDB 以及其他依賴項目。
準備好了嗎?如果你在 Xcode 中打開了剛剛下載的初始工程,那么請先關(guān)閉。
安裝 SwiftyDB
第一件事情就是下載 SwiftyDB,然后在工程中使用。下載庫的文件然后放到工程中可不管用,我們要先安裝 CocoaPods。安裝過程不復雜,不會花費太多時間,即使你從來沒有用過 CocoaPods。詳細內(nèi)容請點擊鏈接。
安裝 CocoaPods
我們要將 CocoaPods 安裝到系統(tǒng)中,如果你已經(jīng)安裝了 CocoaPods,那么請?zhí)^這一步,如果沒有,那么打開 Terminal 終端 ,輸入下列命令:
sudo gem install cocoapods
然后按回車,輸入 Mac 密碼,等一會然后開始下載,下載完畢后不要關(guān)閉 Terminal 終端 ,我們之后還會用到。
安裝 SwiftyDB 和其他的依賴庫
使用 cd 命令找到初始工程對應(yīng)的文件夾(仍然是在 Terminal 終端中進行操作)。
cd PATH_TO_THE_STARTER_PROJECT_DIRECTORY
現(xiàn)在可以創(chuàng)建 Podfile 文件了,我們在 Podfile 里寫出我們需要的下載的庫。最簡單的方法是輸入下列命名,讓 CocoaPods 給我們創(chuàng)建一個 Podfile。
pod init
一個名為 Podfile 的文件就創(chuàng)建好了,在工程文件夾里,打開 Podfile,最好使用文本編輯軟件(最好不要用 TextEdit 這個軟件),然后將內(nèi)容修改成:
use_frameworks!
target 'NotesDB' do
pod "SwiftyDB"
end
<center>

</center>
這行代碼實際上就做了 pod "swiftyDB" 一件事。CocoaPods 會下載 SwiftyDB 庫和所有的依賴庫,還會創(chuàng)建一些新的子文件夾,以及一個 Xcode workspace。
編輯完 Podfile 文件后,保存關(guān)閉。確保你關(guān)閉了初始工程,回到 Terminail 終端上,輸入下列命令:
pod install
<center>

</center>
安裝完畢之后繼續(xù)。我們這次不再打開初始工程,而是打開 NoteDB.xcworkspace。
<center>

</center>
開始使用 SwiftyDB - 我們的 Model
在 NotesDB 工程中,有個文件叫做 Note.swift,目前還是空的。這就是我們今天要講述的重點內(nèi)容,我們要創(chuàng)建一些類,表示一條筆記的實體,在理論層面上,即將完成的工作就是 iOS MVC 模式里的 Model。
首先需要引入 SwiftyDB 庫,在文件的頭部輸入如下代碼:
import SwiftyDB
現(xiàn)在,聲明最重要的一個類:
class Note: NSObject, Storable {
}
我們使用 SwiftyDB 時,需要遵循幾條規(guī)則,上面這個類的第一行體現(xiàn)出其中兩條:
- 帶有屬性的類如果要用 SwiftyDB 存到數(shù)據(jù)庫,必須是
NSObject類的子類 - 帶有屬性的類如果要用 SwiftyDB 存到數(shù)據(jù)庫,必須必須遵守
Storable協(xié)議(也是一個 SwiftyDB 協(xié)議)。
現(xiàn)在,我們要想一想,這個類需要哪些屬性,這就需要了解 SwiftyDB 的一條新規(guī)則:從數(shù)據(jù)庫中獲取數(shù)據(jù)時,datatypes 屬性必須是這里列出的一種,以便能載入整個 Note 對象,而不是簡單數(shù)據(jù)(比如一個都是字典的數(shù)組)。如果有某個屬性是“不兼容的”數(shù)據(jù)類型,那么我們就需要額外做一些操作,把它們轉(zhuǎn)換成建議的類型(我們在之后會進行詳細的說明)。默認情況下,將數(shù)據(jù)存儲到數(shù)據(jù)庫時,不兼容的數(shù)據(jù)類型的數(shù)據(jù)都會被 SwiftyDB 直接忽略掉。也不會創(chuàng)建對應(yīng)的表單。同樣的,對于我們不想存儲到數(shù)據(jù)庫中的其他屬性,我們也會特殊對待的。
目前需要說明的最后一條要求:遵守 Storable 協(xié)議的類必須執(zhí)行 init 方法:
class Note: NSObject, Storable {
override required init() {
super.init()
}
}
現(xiàn)在,我們已經(jīng)有所需的信息了,下面開始聲明類的屬性吧。有些屬性后面才需要用到,這里先聲明好:
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!
...
}
除了第一個之外,其他的無需多言。對象初始化后(如果數(shù)據(jù)庫不存在)會創(chuàng)建一個新的數(shù)據(jù)庫(名為 notes.sqlite)并自動創(chuàng)建一個表,表單會和擁有正確數(shù)據(jù)類型的屬性匹配。反之,如果數(shù)據(jù)庫已經(jīng)存在了,就會直接打開數(shù)據(jù)庫。
你可能會注意到,上面的屬性都是描述一條筆記和我們想存儲的特性(標題、問題、文字顏色、字體和大小、創(chuàng)建和修改日期),但是唯獨沒有筆記中存儲的圖片。哈哈,我是故意的,我要給圖片單獨創(chuàng)建一個類,只存儲兩個屬性:圖片的名字和尺寸。
所以,繼續(xù)在 Note.swift 文件中創(chuàng)建下列類,放到之前的那個類的上方或者下方皆可:
class ImageDescriptor: NSObject, NSCoding {
var frameData: NSData!
var imageName: String!
}
注意,在類中,圖片的 frame 是一個 NSData 對象,不是 CGRect 對象。必須這樣操作,因為這樣我們可以非常容易的將值存儲到數(shù)據(jù)庫里。過一會你就會看到我們是如何轉(zhuǎn)換的,到時候你就明白為什么我們要使用 NSCoding 協(xié)議。
回到 Note 類,我們聲明一個 ImageDescriptor 數(shù)組,如下文:
class Note: NSObject, Storable {
...
var images: [ImageDescriptor]!
...
}
這里有一個限制,現(xiàn)在是時候提到它了,就是實際上 SwiftyDB 不會把集合存儲到數(shù)據(jù)庫中。簡單來說,我們的 images 數(shù)組永遠不會被存儲到數(shù)據(jù)庫里,我們不得不解決圖片的存儲問題。我們可以使用受支持的數(shù)據(jù)類型中的一個(看我之前提供的連接),而最合適的數(shù)據(jù)類型是 NSData。所以,我們不會把 images 數(shù)組存儲到數(shù)據(jù)庫里,而是存儲下列新的屬性:
class Note: NSObject, Storable {
...
var imageData:NSData!
...
}
但是我們?nèi)绾尾拍軐в?ImageDescriptor 對象的 images 數(shù)組變成 imagesData``NSData 對象呢?恩,答案就是 歸檔(archiving) 這個 images 數(shù)組,使用 NSKeyedArchiver 類生成 NSData 對象。我們在后面會演示如何用代碼實現(xiàn),這里只是介紹一下實現(xiàn)思路,后面再來修改 ImageDescriptor 類。
如你所知,一個類可以被歸檔(在其他編程語言中也就做 序列化(serialized)),只要類的所有屬性都可以被序列化就行。在我們的例子中,這是可行的,因為ImageDescriptor 類里的這兩個屬性的數(shù)據(jù)類型(NSData 和 String)是可以被序列化的。然而這還不夠,因為我們還必須要 編碼(encode) 和 解碼(decode) 它們,以便于歸檔和解壓(unarchive),這也就是我們需要 NSCoding 協(xié)議的原因。有了 NSCoding 協(xié)議,我們可以引進如下方法(其中一個就是 init 方法),從而能恰當?shù)鼐幋a和解碼這兩個屬性:
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")
}
}
更多關(guān)于 NSCoding 協(xié)議和 NSKeyedArchiver 類的信息請參見這里和這里,我們不會在這里討論。
除此之外,我們定義一個便利的自定義的 init 方法。代碼非常簡單,一看就懂:
class ImageDescriptor: NSObject, NSCoding {
...
init(frameData: NSData!, imageName: String!) {
super.init()
self.frameData = frameData
self.imageName = imageName
}
}
在這一節(jié)中我們快速介紹了 SwiftyDB 庫。雖然我們還沒有大量使用 SwiftyDB,但是這部分很重要,因為它包含三個要點:
- 創(chuàng)建一個能使用 SwiftyDB 庫的類。
- 了解一些在使用 SwiftyDB 庫時的規(guī)則。
- 了解一些有關(guān)數(shù)據(jù)類型的限制要求,哪些數(shù)據(jù)類型可以被存儲到 SwiftyDB里。
注意:如果你在 Xcode 中看到錯誤提示,立即 Build 工程(Command + B),錯誤提示就會消失了。
主鍵和忽略屬性
在和數(shù)據(jù)庫打交道時,強烈推薦使用 主鍵(primary keys),它們能夠幫你在數(shù)據(jù)庫表中創(chuàng)建獨一無二的標識符,進行各種各樣的操作(例如,更新某個數(shù)據(jù))。你可以在這里找到有關(guān)主鍵的定義。
在 SwiftyDB 數(shù)據(jù)庫中,將類中的某個或某些屬性定義為主鍵的操作非常簡單,庫里提供了 PrimaryKeys 協(xié)議,所有類都應(yīng)該實現(xiàn)這個協(xié)議,從而讓對應(yīng)的表中有主鍵,這樣對象才能有獨一無二的標識符。實現(xiàn)方法非常簡單,動手吧。
在 NotesDB 工程中找到名為 Extensions.swift 的文件,點擊打開,加入下列代碼:
extension Note: PrimaryKeys {
class func primaryKeys() -> Set<String> {
return ["noteID"]
}
}
在我們的 demo 里,我想讓 noteID 屬性成為 sqlite 數(shù)據(jù)庫對應(yīng)的表里唯一的主鍵。如果需要更多的主鍵,用逗號分隔即可(比如,return ["key1","key2","key3"])。
除此之外,并不是類中所有的屬性都要存儲到數(shù)據(jù)庫中,你應(yīng)該明確指出哪些不存儲。例如,在 Note 類中,我們有兩個屬性是不存儲到數(shù)據(jù)庫里的(要么就是不能被存儲,要么就是我們不想存儲):images 數(shù)組和 database 對象。我們?nèi)绾蚊鞔_地排除這兩個屬性呢?引入 SwiftyDB 提供的另外一個協(xié)議:IgnoredPropertie:
extension Note: IgnoredProperties {
class func ignoredProperties() -> Set<String> {
return ["images","database"]
}
}
如果還有更多屬性我們不想存儲到數(shù)據(jù)庫中,那么也需要添加到上面的代碼中,例如,假設(shè)我們有這么一個屬性:
var noteAuthor: String!
我們不想把它存儲到數(shù)據(jù)庫中,這就需要把這個屬性添加到 IgnoredProperties 協(xié)議里:
extension Note: IgnoredProperties {
class func ignoredProperties() -> Set<String> {
return ["images","database","noteAuthor"]
}
}
保存一個新筆記
我們在 Note 里已經(jīng)做了很多工作,是時候回到 demo app 的功能了。我們還沒有給新的類添加任何方法呢,接下來就做這件事,補全所有缺失的功能。
首先要有筆記,需要告訴 App 如何正確地使用 SwiftyDB 來保存筆記和兩個新創(chuàng)建的類。大部分的操作會在 EditNoteViewController.swift 中實現(xiàn),打開此文件,在寫代碼之前,我先列出幾條特別重要的屬性:
-
imageViews:這個數(shù)組里有所有的 image view 對象,對象里有所有添加到筆記的圖片。這個數(shù)組已經(jīng)存在了,過會就能發(fā)現(xiàn)它的強大作用。 -
currentFontName:里面有應(yīng)用于文本的字體名字。 -
currentFontSize:里面是文本的字體的字號。 -
editedNoteID:即將更新內(nèi)容的筆記的noteID值(primary key)。一會兒我們就會用到。
基礎(chǔ)的功能已經(jīng)在初始工程中提前寫好了,我們需要做的就是補全缺失的 saveNote() 方法中的邏輯。首先做兩件事情:一、如果筆記沒有標題或者筆記沒有內(nèi)容,那么,不允許用戶保存筆記。二、在保存筆記時,隱藏鍵盤。如下:
func saveNote() {
if txtTitle.text?.characters.count == 0 || tvNote.text.characters.count == 0 {
return
}
if tvNote.isFirstResponder() {
tvNote.resignFirstResponder()
}
}
繼續(xù)初始化一個新的 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()
}
現(xiàn)在稍微解釋一下上面的代碼:
noteID屬性需要 Int 類型的數(shù)字作為主鍵。你可以創(chuàng)建生成任何你想要的值,只要它們是獨一無二的。在這里,我們把當前時間戳作為我們的主鍵,不過在實際的應(yīng)用開發(fā)中這不是一個好主意,因為時間戳包含了太多數(shù)字。然而對我們目前的這個應(yīng)用來說,時間戳還是一個不錯的選擇,畢竟這是創(chuàng)建獨一無二數(shù)值最簡單的方法。當我們第一次存儲一條新筆記時,把當前時間(也就是 NSDate 對象)設(shè)置為創(chuàng)建日期和修改日期。
這里唯一需要特殊處理的行為是將文本顏色轉(zhuǎn)換成 NSData 對象,通過使用
NSKeyedArchiver類來存儲顏色對象。
接下來看如何存儲圖片。我們創(chuàng)建一個新的方法來處理圖片數(shù)組。這個方法主要做兩件事:將實際圖片存儲到應(yīng)用的 documents 目錄下,給每個圖片創(chuàng)建 ImageDescriptor 對象并添加到 images 數(shù)組里。
在實現(xiàn)這個方法之前,我們先要修改一下 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())
}
}
上面這個方法到底做了什么呢:
- 首先,我們確認
images數(shù)組是否存在。如果為空,進行初始化,如果存在,我們只需要將里面的數(shù)據(jù)清除即可,在更新既有的筆記時,第二個方法在會非常有用。 - 然后對每個圖片我們創(chuàng)建一個獨一無二的名字,每個名字都類似這樣:“img_12345679_1”。
- 使用
init方法初始化一個新的ImageDescriptor方法, image view 的 frame 和名字是該方法的參數(shù)。toNSData()方法已經(jīng)實現(xiàn)好了,是CGRect的擴展,你可以從Extensions.swift文件里找到。目的是將 frame 轉(zhuǎn)換成NSData對象。一旦新的ImageDescriptor對象準備好了,就可以添加到images數(shù)組里了。 - 我們將實際的圖片存儲到 documents 目錄下,
saveImage(_: withName:)類方法可以在Helper.swift文件里找到,這里還有很多有用的類方法。 - 最后,當所有的 image views 都處理過后,通過 archiving(歸檔)我們將
images數(shù)組轉(zhuǎn)換成NSData對象,存儲到imagesData屬性里。上面代碼中的最后一行,是NSCoding協(xié)議必須實現(xiàn)的方法。
上面的 else 看起來似乎有些多余,實際上很有用。默認情況下,imagesData 為空,如果某條筆記里沒有添加圖片,就會一直為空直。然而,SQLite 不識別 nil(空),SQLite 理解的是 NSNull,也就是轉(zhuǎn)換成 NSData 對象。
回到 EditNoteViewController.swift 文件中,用上我們剛剛創(chuàng)建的方法:
func saveNote() {
...
note.storeNoteImagesFromImageViews(imageViews)
}
現(xiàn)在回到 Note.swift,實現(xiàn)實際存儲到數(shù)據(jù)庫的方法。這里有個重點:SwiftyDB 可以同步或異步執(zhí)行任何數(shù)據(jù)庫相關(guān)操作,選擇哪種方法取決于應(yīng)用的性質(zhì)。然而,我建議使用異步方法,這樣在進行數(shù)據(jù)庫操作時,不會阻塞主線程,也不會出現(xiàn) UI 控件突然卡住這種不好的用戶體驗。不過我還是再強調(diào)一次,選擇哪種方法,完全由你決定。
這里我們用異步方式來存儲數(shù)據(jù)。如你所見,每個 SwiftyDB 方法都包含一個閉包,可以返回執(zhí)行結(jié)果。你可以在這里閱讀相關(guān)的信息,實際上,我建議你現(xiàn)在先去閱讀。
現(xiàn)在來實現(xiàn)我們的新方法:
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)
}
}
}
從上面的實現(xiàn)方法可以知道,我們要使用相同的方法來更新筆記。把 shouldUpdate 設(shè)置為布爾值,作為該方法的參數(shù),然后根據(jù) asyncDataObject 的值來判斷是否創(chuàng)建一個新的筆記,或者更新一個已存在的筆記。
此外,第二個參數(shù)是 completion handler。能否用合適的參數(shù)值調(diào)用它,取決于我們的存儲是否成功。當你的任務(wù)在后臺使用異步方法時,我建議你使用 completion handler。這樣,當任務(wù)完成后,你就能通知調(diào)用方法,將任何結(jié)果或者數(shù)據(jù)調(diào)回來。
上面你看到的這些,其他的數(shù)據(jù)庫相關(guān)方法中也有。我們會先檢查錯誤,然后根據(jù)是否存在結(jié)果來執(zhí)行下一步的操作。在上面的例子中,如果出現(xiàn)錯誤,我們就可以調(diào)用 completion handler,傳入 false 值,意味著存儲失敗,反之,我們傳入 true 值,表示操作成功。
回到 EditNoteViewController 類,完成 saveNote() 方法。調(diào)用上面創(chuàng)建的方法,如果筆記存儲成功了,pop 當前的 view controller,如果存儲發(fā)生了錯誤,我們顯示一段提示信息。
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 屬性是否為空,也就是筆記是否被更新。
現(xiàn)在,你可以運行 App 然后試著存儲一條新筆記了。如果你是按照上面一步一步走到現(xiàn)在的,那么存儲筆記功能已經(jīng)可以正常使用了。
下載和列出筆記
創(chuàng)建和存儲新筆記的功能已經(jīng)實現(xiàn)了,我們可以繼續(xù)開發(fā)讀取筆記功能了。讀取筆記意味著將筆記列在 NoteListViewController 類中,在我們正式開始之前,先在 Note.swift 文件里讀取數(shù)據(jù)。
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 里執(zhí)行讀取功能的方法是 asyncObjectsForType(...),是一個異步執(zhí)行的方法。結(jié)果要么是一個錯誤,要么就是從數(shù)據(jù)庫里讀取一個 note 對象集合(數(shù)組)。在第一種情況下,我們調(diào)用 completion handler 傳入 nil,告訴調(diào)用者這里在讀取數(shù)據(jù)時遇到了問題。在第二種情況下,把 'Note' 對象傳入 completion handler,這樣可以在方法之外使用它們。
現(xiàn)在回到 NoteListViewController.swift 文件,首先必須聲明一個數(shù)組包含 Note 對象(剛剛從數(shù)據(jù)庫中讀取出來)。這個數(shù)組就是 tableview 的 datasource(很明顯嘛)。所以,在類的開頭,加入下列代碼:
var notes = [Note]()
除此之外,初始化一個新的 Note 對象,可以使用之前創(chuàng)建的 loadAllNotes(...) 方法:
var note = Note()
是時候?qū)懸粋€簡單的新方法了,調(diào)用上面的方法,讀取所有存儲在數(shù)據(jù)庫中的對象,放到 notes 數(shù)組里。
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 數(shù)組里。
上面的兩個方法就是我們所需的全部方法。有了這兩個方法,我們就能從數(shù)據(jù)庫里得到之前存儲的筆記。別忘了,loadNotes() 必須在某個地方被調(diào)用,我們在 viewDidLoad() 方法中調(diào)用 loadNotes() 。
override func viewDidLoad() {
...
loadNotes()
}
光是讀取筆記還不夠,讀取筆記數(shù)據(jù)之后還要使用這些數(shù)據(jù)。我們先更新 tableview 的相關(guān)方法,從行數(shù)開始:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return notes.count
}
接下來我們把筆記的數(shù)據(jù)放到 tableview 中,具體說來,我們會展示筆記的標題、創(chuàng)建筆記和修改筆記的日期。
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
}
現(xiàn)在運行應(yīng)用吧,你創(chuàng)建的所有筆記都會出現(xiàn)在 tableview 中了。
另外一種獲取數(shù)據(jù)的方法
現(xiàn)在我們是用 asyncObjectsForType(...) 方法來加載數(shù)據(jù)庫中所有的筆記。如你所知,這個方法會返回一個數(shù)組對象(在我們的例子里,就是 Note 對象),我覺得這個方法特別有用,但并不能適應(yīng)所有情況。某些情況下,讀取實際的數(shù)值數(shù)據(jù)會更方便。
這一點 SwiftyDB 也能做到,它提供了另外一種方法來獲取數(shù)據(jù):asyncDataForType(...) (或 dataForType(...),如果你想使用同步操作的話)。它會返回一個字典類型的集合,格式 [[String: SQLiteVlalue]](在這里 SQLiteVlalue 是任何一種支持的數(shù)據(jù)類型)。
你可以在這里和這里找到更多的信息,我把這個任務(wù)留給你,作為一個練習:修改 Note 類,加載簡單的數(shù)據(jù)和數(shù)值,而不是只加載對象。
更新一條筆記
我們還想讓應(yīng)用具有編輯筆記的功能,換句話說,當用戶點擊某一行時,我們就顯示 EditNoteViewController 界面,其中包含這條筆記的所有信息;用戶修改之后保存,我們需要存儲筆記修改后的信息。
首先,在 NoteListViewController.swift 文件里,我們需要一個新的屬性來存儲所選筆記的 ID,所以我們在類的頂部寫入下列代碼:
var idOfNoteToEdit: Int!
下面我們來實現(xiàn)一個 UITableViewDelegate 方法,根據(jù)所有的行找到對應(yīng)的 noteID 值,通過 segue 來顯示 EditViewContrller:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
idOfNoteToEdit = notes[indexPath.row].noteID as Int
performSegueWithIdentifier("idSegueEditNote", sender: self)
}
在 prepareForSegue(...) 方法里,我們把 idOfNoteToEdit 值傳給接下來出現(xiàn)的 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
}
}
}
}
到這里我們已經(jīng)完成了一半的工作了,在我們回到 EditNoteViewController 類之前,先去 Note 類里實現(xiàn)一個簡單的新方法,能通過輸入的 ID 值取回單條筆記的信息,下面是實現(xiàn)方法:
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 方法來對返回的結(jié)果進行過濾。使用 Filter 類里的 equal(...) 方法可以設(shè)置我們想要的過濾條件。別忘了看一下這個鏈接,里面有更多實現(xiàn)過濾的方法(在從數(shù)據(jù)庫里取數(shù)據(jù)或者對象時)。
通過上面的過濾方法,我們實際上可以讓 SwiftyDB 只加載符合條件的筆記:上面方法中參數(shù)的值對應(yīng)的 noteID 的筆記。當然,只會返回一條筆記,因為我們這里使用的是主鍵,一個主鍵只對應(yīng)一個記錄。
返回的結(jié)果會作為 Note 對象的數(shù)組,所以需要先獲取集合的第一個(唯一一個)元素。然后,必須將 image data(如果存在的話)轉(zhuǎn)換為 ImageDescriptor 對象數(shù)組,然后將其賦值給 images 屬性。這點很重要,如果跳過這一步,下載下來的筆記里的圖片都無法顯示。最后,根據(jù)是否成功取得筆記數(shù)據(jù),我們調(diào)用 completion handler。如果成功取得筆記,我們把讀取來的對象傳給 completion handler,讓調(diào)用者使用,如果沒有成功取得筆記,返回 nil,因為沒有取得對象。
現(xiàn)在,回到 EditNoteViewController.swift 文件,聲明并初始化一個新的 Note 屬性:
var editedNote = Note()
這個對象首先調(diào)用上面實現(xiàn)的新方法,然后存儲從數(shù)據(jù)庫中加載的數(shù)據(jù)。
使用 loadSingleNote(...) 方法來,根據(jù) editedNoteID 屬性來加載特定的某條筆記。對我們而言,我們要定義 viewWillAppear(_:) 方法,在這里我們要擴展一些邏輯。
在下面的代碼中你會看到,loadSingleNotedWithID(...) 會在 completion handler 獲取到筆記之后給所有屬性賦值。也就是說,我們會設(shè)置筆記的標題、內(nèi)容、文字顏色、文字字體等等。不僅如此,如果筆記里有圖片,我們還會給每條筆記創(chuàng)建 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() 方法,這樣當一條已有筆記更新內(nèi)容后,不會創(chuàng)建一條新的 Note 對象,也不會生成一個新的主鍵和創(chuàng)建日期。
所以,找到這三行代碼(在 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()
}
剩下的部分保持不變(至少現(xiàn)在來說是這樣)。
更新筆記列表
如果現(xiàn)在測試 App,你會發(fā)現(xiàn)創(chuàng)建新的筆記或者編輯某條筆記后,筆記清單沒有更新。這很正常,因為你還沒有開發(fā)這個功能呢,在這一節(jié)中,我們會修復這個問題。
你可能已經(jīng)猜到了,我們會使用 代理模式(Delegation pattern) 來通知 NoteListViewController 類,告知 EditViewController 里發(fā)生的變動。我們的出發(fā)點是在 EditViewController 里創(chuàng)建一個新的協(xié)議,協(xié)議包含兩個必須實現(xiàn)的方法,如下:
protocol EditNoteViewControllerDelegate {
func didCreateNewNote(noteID: Int)
func didUpdateNote(noteID: Int)
}
在這兩種情況下,我們都給委托方法提供新的或編輯筆記的 ID 值。現(xiàn)在到 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)
從今往后,每當創(chuàng)建新筆記或者編輯已有筆后,對應(yīng)的 delegate 方法就會被調(diào)用。目前我們只完成了一半的工作,讓我們回到 NoteListViewController.swift 文件,首先在類的開頭遵守新的協(xié)議:
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 // 增加這一行代碼
...
}
}
}
不錯,大部分的工作都完成了。還需要實現(xiàn)兩個協(xié)議方法,我們先處理創(chuàng)建新筆記這種情況:
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()
}
})
}
}
如你所見,我們從數(shù)據(jù)庫里獲取 noteID 參數(shù)值對應(yīng)的對象,然后(如果對象存在)我們把對象添加到 notes 數(shù)組,重新加載 tableview。
繼續(xù)實現(xiàn)另一個操作:
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,找到之后從數(shù)據(jù)庫里加載對應(yīng)的筆記,用新的對象替換舊的對象,然后更新 tableview,新的修改日期就會出現(xiàn)了。
刪除記錄
還有最后一個主要的功能沒有開發(fā),那就是刪除筆記。很明顯,我們需要在 Note 類里實現(xiàn)我們最后一個方法,每次想刪除筆記時都會調(diào)用這個方法。請打開 Note.swift 文件。
這里唯一的一個知識點就是 SwiftyDB 方法會從數(shù)據(jù)庫里直接刪除數(shù)據(jù),在接下來的實現(xiàn)方法中你會看到這一點。和以前一樣,這個操作還是異步操作,一旦執(zhí)行結(jié)束,調(diào)用 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)
}
}
}
現(xiàn)在打開 NoteListViewController.swift,定義下一個方法 UITableViewDataSource:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == UITableViewCellEditingStyle.Delete {
}
}
把上面的方法添加到代碼中之后,每次你左滑一行筆記,右邊會出現(xiàn)默認的 Delete 按鈕。而且,當用戶點擊 Delete 按鈕時,會執(zhí)行 if 后面對應(yīng)的代碼,如下:
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()
}
})
})
}
}
首先,找到所選中行對應(yīng)的對象,然后,調(diào)用 Note 類里的新方法進行刪除,如果刪除成功,從 notes 數(shù)組里移除 Note 對象,重新加載 tableview,更新 UI 顯示內(nèi)容。
就是這么簡單!
那么,如何排序呢?
你可能正在想,如何對讀取出來的數(shù)據(jù)進行排序。排序非常有用,可以基于一個或者多個字段進行升序或降序排列,最后改變返回數(shù)據(jù)的順序。例如,我們可以將我們所有的筆記按照修改日期的先后進行排序。
不幸的是,在我寫這篇教程時,SwiftyDB 還不支持對數(shù)據(jù)進行排序,這確實是個劣勢,不過還有一個解決辦法:手動排序。為了演示手動排序的方法,我們在 NoteListViewController.swift 文件里創(chuàng)建最后一個方法 sortNotes()。這里會使用 Swift 自帶的 sort() 函數(shù):
func sortNotes() {
notes = notes.sort({ (note1, note2) -> Bool in
let modificationDate1 = note1.modificationDate.timeIntervalSinceReferenceDate
let modificationDate2 = note2.modificationDate.timeIntervalSinceReferenceDate
return modificationDate1 > modificationDate2
})
}
由于我們無法直接比較 NSDate 對象,我們先轉(zhuǎn)換成時間戳(double 類型的值)。接著執(zhí)行比較,返回比較的結(jié)果。上面的代碼讓我們進行筆記排序,最新修改的筆記排在 notes 數(shù)組最前面。
只要 notes 數(shù)組發(fā)生了改變,上面的方法就要被調(diào)用。我們先更新 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()
}
})
}
}
現(xiàn)在再運行 App,所有的筆記都會按照它們的修改時間順序顯示。
總結(jié)
毫無疑問,SwiftyDB 是非常棒的工具,可以用在各種應(yīng)用里。非常簡單、高效且可靠,當我們的應(yīng)用必須使用數(shù)據(jù)庫時,SwiftyDB 可以滿足各種需求。在本文的 demo 輔導教程里,我們了解了 SwiftyDB 的基本知識,還有很多東西等待你去學習。當然,如需更多幫助,這里有官方文檔供你查閱。在今天的例子講解中,為了方便編寫輔導教程,我們創(chuàng)建的這個數(shù)據(jù)庫有一個表對應(yīng) Note 類。在實際開發(fā)中,你想創(chuàng)建多少表就能創(chuàng)建多少表,只要有對應(yīng)的 model 代碼即可(對應(yīng)的類)。就我個人而言,我肯定會在我的項目中使用 SwiftyDB 的,實際上,我正在這樣做。現(xiàn)在你已經(jīng)了解了 SwiftyDB,你也見識了它如何工作的,如何實現(xiàn)的。SwiftyDB 能否成為你工具箱里的新成員,完全由你決定??傊蚁M喿x這篇文章并不是在浪費你的時間,希望你也學到了一些新的知識,在我們下一教程出來之前,祝您開心!
僅供參考,你可以在 GitHub 上下載完整的工程
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請訪問 http://swift.gg。