用 SwiftyDB 管理 SQLite 數據庫

做者:AppCoda,原文連接,原文日期:2016-03-16
譯者:Crystal Sun;校對:numbbbbb;定稿:shankshtml

選擇哪一種數據持久化的方式,是咱們在開發 App 時經常遇到的問題。咱們有太多選擇了:建立一個單獨的文件、使用 CoreData 或者建立 SQLite 數據庫。使用 SQLite 數據庫有點麻煩,由於首先要先建立數據庫,提早寫好表和字段。此外,從編程的角度來看,數據的存儲、更新、和獲取都不是很容易的操做。ios

而當咱們使用 GitHub 上的 SwiftyDB 這個第三方庫時,上面的這些問題均可以垂手可得地解決。SwiftyDB,用做者的話來講,就是即插即用型的好幫手。SwiftyDB 將開發者從繁重的手動建立 SQLite 數據庫的工做中解放出來,不再用提早定義好各類表和字段了。SwiftyDB 中類的屬性可以自動完成上述工做,能夠直接用類做爲數據模型。除此以外,全部對數據庫的操做都被封裝起來,開發者能夠把全部的注意力放到應用的邏輯層面上。簡單強悍的 API 可讓處理數據成爲小菜一碟的事情。git

不過須要強調一下,SwiftyDB 並不能創造奇蹟。它只是一個靠譜的第三方庫,能夠很好地完成它該作的事情(雖然有一些特性目前還不具有)。儘管如此,它仍然是一個很是好用的工具,值得你花時間學習。在本篇文章中,咱們將學習 SwiftyDB 的基本使用操做。github

能夠從這裏找到文檔,看完這篇文章後最好再去看看文檔。若是你一直想用 SQLite,但是歷來沒有真正開始,那 SwiftyDB 是一個好的開始。sql

好了,讓咱們開始探索這個全新的、使人期待的工具吧。數據庫

關於 Demo App

在這篇文章中,咱們要建立一個很是簡單的筆記應用,能夠實現以下這些基本操做:編程

  • 列出筆記swift

  • 建立新的筆記數組

  • 更新已經建立的筆記的內容閉包

  • 刪除筆記

很明顯,SwiftyDB 將要管理一個 SQLite 數據庫,上面列出的操做足以向你展現如何使用 SwiftyDB。

簡單起見,我事先建立了一個工程,點擊下載而後打開工程。用 Xcode 打開工程後,可以看到全部的基本功能,不過缺乏與數據有關的代碼。運行項目,你就能看到全貌了。

應用有一個導航欄,在第一個 view controller 中,有一個 tableview 列出全部筆記。

<center>

</center>

點擊某個筆記,咱們能夠編輯更新內容,若是向左滑動某條筆記,能夠刪除筆記:

<center>

</center>

建立一個新筆記只需點擊導航欄上的加號按鈕,下面是咱們在編輯筆記時能夠進行的操做:

  1. 設置筆記的標題和內容。

  2. 更改字體。

  3. 更改字體的大小。

  4. 更改字體的顏色。

  5. 添加圖片。

  6. 移動圖片到另一個位置。

上述全部值的改變都會存儲到數據庫中。在最後兩條中,圖片其實是存儲在應用的 documents directory 中,咱們在數據庫中只是存儲圖片的名字和 frame。此外,咱們還要建立一個類來管理圖片(更多細節參見後面的內容)。

<center>

</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

一個名爲 Podfile 的文件就建立好了,在工程文件夾裏,打開 Podfile,最好使用文本編輯軟件(最好不要用 TextEdit 這個軟件),而後將內容修改爲:

use_frameworks!

target 'NotesDB' do
    pod "SwiftyDB"
end

<center>

</center>

這行代碼實際上就作了 pod "swiftyDB" 一件事。CocoaPods 會下載 SwiftyDB 庫和全部的依賴庫,還會建立一些新的子文件夾,以及一個 Xcode workspace。

編輯完 Podfile 文件後,保存關閉。確保你關閉了初始工程,回到 Terminail 終端上,輸入下列命令:

pod install

<center>

</center>

安裝完畢以後繼續。咱們此次再也不打開初始工程,而是打開 NoteDB.xcworkspace

<center>

</center>

開始使用 SwiftyDB - 咱們的 Model

NotesDB 工程中,有個文件叫作 Note.swift,目前仍是空的。這就是咱們今天要講述的重點內容,咱們要建立一些類,表示一條筆記的實體,在理論層面上,即將完成的工做就是 iOS MVC 模式裏的 Model

首先須要引入 SwiftyDB 庫,在文件的頭部輸入以下代碼:

import SwiftyDB

如今,聲明最重要的一個類:

class Note: NSObject, Storable {

}

咱們使用 SwiftyDB 時,須要遵循幾條規則,上面這個類的第一行體現出其中兩條:

  1. 帶有屬性的類若是要用 SwiftyDB 存到數據庫,必須是 NSObject 類的子類

  2. 帶有屬性的類若是要用 SwiftyDB 存到數據庫,必須必須遵照 Storable 協議(也是一個 SwiftyDB 協議)。

如今,咱們要想想,這個類須要哪些屬性,這就須要瞭解 SwiftyDB 的一條新規則:從數據庫中獲取數據時,datatypes 屬性必須是這裏列出的一種,以便能載入整個 Note 對象,而不是簡單數據(好比一個都是字典的數組)。若是有某個屬性是「不兼容的」數據類型,那麼咱們就須要額外作一些操做,把它們轉換成建議的類型(咱們在以後會進行詳細的說明)。默認狀況下,將數據存儲到數據庫時,不兼容的數據類型的數據都會被 SwiftyDB 直接忽略掉。也不會建立對應的表單。一樣的,對於咱們不想存儲到數據庫中的其餘屬性,咱們也會特殊對待的。

目前須要說明的最後一條要求:遵照 Storable 協議的類必須執行 init 方法:

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

如今,咱們已經有所需的信息了,下面開始聲明類的屬性吧。有些屬性後面才須要用到,這裏先聲明好:

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

除了第一個以外,其餘的無需多言。對象初始化後(若是數據庫不存在)會建立一個新的數據庫(名爲 notes.sqlite)並自動建立一個表,表單會和擁有正確數據類型的屬性匹配。反之,若是數據庫已經存在了,就會直接打開數據庫。

你可能會注意到,上面的屬性都是描述一條筆記和咱們想存儲的特性(標題、問題、文字顏色、字體和大小、建立和修改日期),可是惟獨沒有筆記中存儲的圖片。哈哈,我是故意的,我要給圖片單首創建一個類,只存儲兩個屬性:圖片的名字和尺寸。

因此,繼續在 Note.swift 文件中建立下列類,放到以前的那個類的上方或者下方皆可:

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

注意,在類中,圖片的 frame 是一個 NSData 對象,不是 CGRect 對象。必須這樣操做,由於這樣咱們能夠很是容易的將值存儲到數據庫裏。過一會你就會看到咱們是如何轉換的,到時候你就明白爲何咱們要使用 NSCoding 協議。

回到 Note 類,咱們聲明一個 ImageDescriptor 數組,以下文:

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

這裏有一個限制,如今是時候提到它了,就是實際上 SwiftyDB 不會把集合存儲到數據庫中。簡單來講,咱們的 images 數組永遠不會被存儲到數據庫裏,咱們不得不解決圖片的存儲問題。咱們可使用受支持的數據類型中的一個(看我以前提供的鏈接),而最合適的數據類型是 NSData。因此,咱們不會把 images 數組存儲到數據庫裏,而是存儲下列新的屬性:

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

可是咱們如何才能將帶有 ImageDescriptor 對象的 images 數組變成 imagesData`NSData 對象呢?恩,答案就是 歸檔(archiving) 這個 images 數組,使用 NSKeyedArchiver 類生成 NSData 對象。咱們在後面會演示如何用代碼實現,這裏只是介紹一下實現思路,後面再來修改 ImageDescriptor` 類。

如你所知,一個類能夠被歸檔(在其餘編程語言中也就作 序列化(serialized)),只要類的全部屬性均可以被序列化就行。在咱們的例子中,這是可行的,由於ImageDescriptor 類裏的這兩個屬性的數據類型(NSDataString)是能夠被序列化的。然而這還不夠,由於咱們還必需要 編碼(encode)解碼(decode) 它們,以便於歸檔和解壓(unarchive),這也就是咱們須要 NSCoding 協議的緣由。有了 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 中看到錯誤提示,當即 Build 工程(Command + B),錯誤提示就會消失了。

主鍵和忽略屬性

在和數據庫打交道時,強烈推薦使用 主鍵(primary keys),它們可以幫你在數據庫表中建立獨一無二的標識符,進行各類各樣的操做(例如,更新某個數據)。你能夠在這裏找到有關主鍵的定義。

在 SwiftyDB 數據庫中,將類中的某個或某些屬性定義爲主鍵的操做很是簡單,庫裏提供了 PrimaryKeys 協議,全部類都應該實現這個協議,從而讓對應的表中有主鍵,這樣對象纔能有獨一無二的標識符。實現方法很是簡單,動手吧。

NotesDB 工程中找到名爲 Extensions.swift 的文件,點擊打開,加入下列代碼:

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

在咱們的 demo 裏,我想讓 noteID 屬性成爲 sqlite 數據庫對應的表裏惟一的主鍵。若是須要更多的主鍵,用逗號分隔便可(好比,return ["key1","key2","key3"])。

除此以外,並非類中全部的屬性都要存儲到數據庫中,你應該明確指出哪些不存儲。例如,在 Note 類中,咱們有兩個屬性是不存儲到數據庫裏的(要麼就是不能被存儲,要麼就是咱們不想存儲):images 數組和 database 對象。咱們如何明確地排除這兩個屬性呢?引入 SwiftyDB 提供的另一個協議:IgnoredPropertie

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

若是還有更多屬性咱們不想存儲到數據庫中,那麼也須要添加到上面的代碼中,例如,假設咱們有這麼一個屬性:

var noteAuthor: String!

咱們不想把它存儲到數據庫中,這就須要把這個屬性添加到 IgnoredProperties 協議裏:

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

保存一個新筆記

咱們在 Note 裏已經作了不少工做,是時候回到 demo app 的功能了。咱們尚未給新的類添加任何方法呢,接下來就作這件事,補全全部缺失的功能。

首先要有筆記,須要告訴 App 如何正確地使用 SwiftyDB 來保存筆記和兩個新建立的類。大部分的操做會在 EditNoteViewController.swift 中實現,打開此文件,在寫代碼以前,我先列出幾條特別重要的屬性:

  • imageViews:這個數組裏有全部的 image view 對象,對象裏有全部添加到筆記的圖片。這個數組已經存在了,過會就能發現它的強大做用。

  • currentFontName:裏面有應用於文本的字體名字。

  • currentFontSize:裏面是文本的字體的字號。

  • editedNoteID:即將更新內容的筆記的 noteID 值(primary key)。一下子咱們就會用到。

基礎的功能已經在初始工程中提早寫好了,咱們須要作的就是補全缺失的 saveNote() 方法中的邏輯。首先作兩件事情:1、若是筆記沒有標題或者筆記沒有內容,那麼,不容許用戶保存筆記。2、在保存筆記時,隱藏鍵盤。以下:

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

繼續初始化一個新的 Note 對象,給各個屬性賦值。images 屬性須要特殊對待,咱們在後邊再處理。

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

如今稍微解釋一下上面的代碼:

  • noteID 屬性須要 Int 類型的數字做爲主鍵。你能夠建立生成任何你想要的值,只要它們是獨一無二的。在這裏,咱們把當前時間戳做爲咱們的主鍵,不過在實際的應用開發中這不是一個好主意,由於時間戳包含了太多數字。然而對咱們目前的這個應用來講,時間戳仍是一個不錯的選擇,畢竟這是建立獨一無二數值最簡單的方法。

  • 當咱們第一次存儲一條新筆記時,把當前時間(也就是 NSDate 對象)設置爲建立日期和修改日期。

  • 這裏惟一須要特殊處理的行爲是將文本顏色轉換成 NSData 對象,經過使用 NSKeyedArchiver 類來存儲顏色對象。

接下來看如何存儲圖片。咱們建立一個新的方法來處理圖片數組。這個方法主要作兩件事:將實際圖片存儲到應用的 documents 目錄下,給每一個圖片建立 ImageDescriptor 對象並添加到 images 數組裏。

在實現這個方法以前,咱們先要修改一下 Note.swift 文件。先看代碼:

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

上面這個方法到底作了什麼呢:

  1. 首先,咱們確認 images 數組是否存在。若是爲空,進行初始化,若是存在,咱們只須要將裏面的數據清除便可,在更新既有的筆記時,第二個方法在會很是有用。

  2. 而後對每一個圖片咱們建立一個獨一無二的名字,每一個名字都相似這樣:「img_12345679_1」。

  3. 使用 init 方法初始化一個新的 ImageDescriptor 方法, image view 的 frame 和名字是該方法的參數。toNSData() 方法已經實現好了,是 CGRect 的擴展,你能夠從 Extensions.swift 文件裏找到。目的是將 frame 轉換成 NSData 對象。一旦新的 ImageDescriptor 對象準備好了,就能夠添加到 images 數組裏了。

  4. 咱們將實際的圖片存儲到 documents 目錄下,saveImage(_: withName:) 類方法能夠在 Helper.swift 文件裏找到,這裏還有不少有用的類方法。

  5. 最後,當全部的 image views 都處理事後,經過 archiving(歸檔)咱們將 images 數組轉換成 NSData 對象,存儲到 imagesData 屬性裏。上面代碼中的最後一行,是 NSCoding 協議必須實現的方法。

上面的 else 看起來彷佛有些多餘,實際上頗有用。默認狀況下,imagesData 爲空,若是某條筆記裏沒有添加圖片,就會一直爲空直。然而,SQLite 不識別 nil(空),SQLite 理解的是 NSNull,也就是轉換成 NSData 對象。

回到 EditNoteViewController.swift 文件中,用上咱們剛剛建立的方法:

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

如今回到 Note.swift,實現實際存儲到數據庫的方法。這裏有個重點:SwiftyDB 能夠同步或異步執行任何數據庫相關操做,選擇哪一種方法取決於應用的性質。然而,我建議使用異步方法,這樣在進行數據庫操做時,不會阻塞主線程,也不會出現 UI 控件忽然卡住這種很差的用戶體驗。不過我仍是再強調一次,選擇哪一種方法,徹底由你決定。

這裏咱們用異步方式來存儲數據。如你所見,每一個 SwiftyDB 方法都包含一個閉包,能夠返回執行結果。你能夠在這裏閱讀相關的信息,實際上,我建議你如今先去閱讀。

如今來實現咱們的新方法:

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

從上面的實現方法能夠知道,咱們要使用相同的方法來更新筆記。把 shouldUpdate 設置爲布爾值,做爲該方法的參數,而後根據 asyncDataObject 的值來判斷是否建立一個新的筆記,或者更新一個已存在的筆記。

此外,第二個參數是 completion handler。可否用合適的參數值調用它,取決於咱們的存儲是否成功。當你的任務在後臺使用異步方法時,我建議你使用 completion handler。這樣,當任務完成後,你就能通知調用方法,將任何結果或者數據調回來。

上面你看到的這些,其餘的數據庫相關方法中也有。咱們會先檢查錯誤,而後根據是否存在結果來執行下一步的操做。在上面的例子中,若是出現錯誤,咱們就能夠調用 completion handler,傳入 false 值,意味着存儲失敗,反之,咱們傳入 true 值,表示操做成功。

回到 EditNoteViewController 類,完成 saveNote() 方法。調用上面建立的方法,若是筆記存儲成功了,pop 當前的 view controller,若是存儲發生了錯誤,咱們顯示一段提示信息。

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

注意上面方法中的 shouldUpdate 變量,它可否獲得合適的值,取決於 editedNoteID 屬性是否爲空,也就是筆記是否被更新。

如今,你能夠運行 App 而後試着存儲一條新筆記了。若是你是按照上面一步一步走到如今的,那麼存儲筆記功能已經能夠正常使用了。

下載和列出筆記

建立和存儲新筆記的功能已經實現了,咱們能夠繼續開發讀取筆記功能了。讀取筆記意味着將筆記列在 NoteListViewController 類中,在咱們正式開始以前,先在 Note.swift 文件裏讀取數據。

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

SwiftyDB 裏執行讀取功能的方法是 asyncObjectsForType(...),是一個異步執行的方法。結果要麼是一個錯誤,要麼就是從數據庫裏讀取一個 note 對象集合(數組)。在第一種狀況下,咱們調用 completion handler 傳入 nil,告訴調用者這裏在讀取數據時遇到了問題。在第二種狀況下,把 'Note' 對象傳入 completion handler,這樣能夠在方法以外使用它們。

如今回到 NoteListViewController.swift 文件,首先必須聲明一個數組包含 Note 對象(剛剛從數據庫中讀取出來)。這個數組就是 tableview 的 datasource(很明顯嘛)。因此,在類的開頭,加入下列代碼:

var notes = [Note]()

除此以外,初始化一個新的 Note 對象,可使用以前建立的 loadAllNotes(...) 方法:

var note = Note()

是時候寫一個簡單的新方法了,調用上面的方法,讀取全部存儲在數據庫中的對象,放到 notes 數組裏。

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

請注意,在讀取全部的筆記後用主線程從新加載 tableview.固然,在重載以前,把全部的筆記存到 notes 數組裏。

上面的兩個方法就是咱們所需的所有方法。有了這兩個方法,咱們就能從數據庫裏獲得以前存儲的筆記。別忘了,loadNotes() 必須在某個地方被調用,咱們在 viewDidLoad() 方法中調用 loadNotes()

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

光是讀取筆記還不夠,讀取筆記數據以後還要使用這些數據。咱們先更新 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 中了。

另一種獲取數據的方法

如今咱們是用 asyncObjectsForType(...) 方法來加載數據庫中全部的筆記。如你所知,這個方法會返回一個數組對象(在咱們的例子裏,就是 Note 對象),我以爲這個方法特別有用,但並不能適應全部狀況。某些狀況下,讀取實際的數值數據會更方便。

這一點 SwiftyDB 也能作到,它提供了另一種方法來獲取數據:asyncDataForType(...) (或 dataForType(...),若是你想使用同步操做的話)。它會返回一個字典類型的集合,格式 [[String: SQLiteVlalue]](在這裏 SQLiteVlalue 是任何一種支持的數據類型)。

你能夠在這裏這裏找到更多的信息,我把這個任務留給你,做爲一個練習:修改 Note 類,加載簡單的數據和數值,而不是隻加載對象。

更新一條筆記

咱們還想讓應用具備編輯筆記的功能,換句話說,當用戶點擊某一行時,咱們就顯示 EditNoteViewController 界面,其中包含這條筆記的全部信息;用戶修改以後保存,咱們須要存儲筆記修改後的信息。

首先,在 NoteListViewController.swift 文件裏,咱們須要一個新的屬性來存儲所選筆記的 ID,因此咱們在類的頂部寫入下列代碼:

var idOfNoteToEdit: Int!

下面咱們來實現一個 UITableViewDelegate 方法,根據全部的行找到對應的 noteID 值,經過 segue 來顯示 EditViewContrller

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

prepareForSegue(...) 方法裏,咱們把 idOfNoteToEdit 值傳給接下來出現的 view controller:

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

到這裏咱們已經完成了一半的工做了,在咱們回到 EditNoteViewController 類以前,先去 Note 類裏實現一個簡單的新方法,能經過輸入的 ID 值取回單條筆記的信息,下面是實現方法:

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

這裏有個新東西,咱們首次使用 filter 方法來對返回的結果進行過濾。使用 Filter 類裏的 equal(...) 方法能夠設置咱們想要的過濾條件。別忘了看一下這個連接,裏面有更多實現過濾的方法(在從數據庫裏取數據或者對象時)。

經過上面的過濾方法,咱們實際上可讓 SwiftyDB 只加載符合條件的筆記:上面方法中參數的值對應的 noteID 的筆記。固然,只會返回一條筆記,由於咱們這裏使用的是主鍵,一個主鍵只對應一個記錄。

返回的結果會做爲 Note 對象的數組,因此須要先獲取集合的第一個(惟一一個)元素。而後,必須將 image data(若是存在的話)轉換爲 ImageDescriptor 對象數組,而後將其賦值給 images 屬性。這點很重要,若是跳過這一步,下載下來的筆記裏的圖片都沒法顯示。最後,根據是否成功取得筆記數據,咱們調用 completion handler。若是成功取得筆記,咱們把讀取來的對象傳給 completion handler,讓調用者使用,若是沒有成功取得筆記,返回 nil,由於沒有取得對象。

如今,回到 EditNoteViewController.swift 文件,聲明並初始化一個新的 Note 屬性:

var editedNote = Note()

這個對象首先調用上面實現的新方法,而後存儲從數據庫中加載的數據。

使用 loadSingleNote(...) 方法來,根據 editedNoteID 屬性來加載特定的某條筆記。對咱們而言,咱們要定義 viewWillAppear(_:) 方法,在這裏咱們要擴展一些邏輯。

在下面的代碼中你會看到,loadSingleNotedWithID(...) 會在 completion handler 獲取到筆記以後給全部屬性賦值。也就是說,咱們會設置筆記的標題、內容、文字顏色、文字字體等等。不只如此,若是筆記裏有圖片,咱們還會給每條筆記建立 images view 控件,控件的大小使用的固然是 ImageDescriptor 對象裏具體的 frames 值。

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

在全部屬性都被賦值後,不要忘了把 note 賦值給 editedNote 對象,後面咱們會用到。

這裏還須要最後一步:更新 saveNote() 方法,這樣當一條已有筆記更新內容後,不會建立一條新的 Note 對象,也不會生成一個新的主鍵和建立日期。

因此,找到這三行代碼(在 saveNote() 方法裏):

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

替換成下面這堆代碼:

let note = (editedNoteID == nil) ? Note() : editedNote

if editedNoteID == nil {
    note.noteID = Int(NSDate().timeIntervalSince1970)
    note.creationDate = NSDate()
}

剩下的部分保持不變(至少如今來講是這樣)。

更新筆記列表

若是如今測試 App,你會發現建立新的筆記或者編輯某條筆記後,筆記清單沒有更新。這很正常,由於你尚未開發這個功能呢,在這一節中,咱們會修復這個問題。

你可能已經猜到了,咱們會使用 代理模式(Delegation pattern) 來通知 NoteListViewController 類,告知 EditViewController 裏發生的變更。咱們的出發點是在 EditViewController 裏建立一個新的協議,協議包含兩個必須實現的方法,以下:

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

在這兩種狀況下,咱們都給委託方法提供新的或編輯筆記的 ID 值。如今到 EditNoteViewController 類,添加下列屬性:

var delegate: EditNoteViewControllerDelegate!

最後,咱們最後一次修改 saveNote() 方法,首先找到 completion handler 閉包:

lf.navigationController?.popViewControllerAnimated(true)

將上面這行代碼刪掉,換成下方這堆的代碼:

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

從今日後,每當建立新筆記或者編輯已有筆後,對應的 delegate 方法就會被調用。目前咱們只完成了一半的工做,讓咱們回到 NoteListViewController.swift 文件,首先在類的開頭遵照新的協議:

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

接下來,在 prepareForSegue(...) 方法裏,讓 NoteListViewController 類成爲 EditNoteViewController 的委託對象。在 let editNoteViewController = segue.destinationViewController as! EditNoteViewController 這行增長下方代碼:

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

不錯,大部分的工做都完成了。還須要實現兩個協議方法,咱們先處理建立新筆記這種狀況:

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

如你所見,咱們從數據庫裏獲取 noteID 參數值對應的對象,而後(若是對象存在)咱們把對象添加到 notes 數組,從新加載 tableview。

繼續實現另外一個操做:

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

在這種狀況下,咱們首先在 notes 字典裏找到被編輯過筆記的 index,找到以後從數據庫里加載對應的筆記,用新的對象替換舊的對象,而後更新 tableview,新的修改日期就會出現了。

刪除記錄

還有最後一個主要的功能沒有開發,那就是刪除筆記。很明顯,咱們須要在 Note 類裏實現咱們最後一個方法,每次想刪除筆記時都會調用這個方法。請打開 Note.swift 文件。

這裏惟一的一個知識點就是 SwiftyDB 方法會從數據庫裏直接刪除數據,在接下來的實現方法中你會看到這一點。和之前同樣,這個操做仍是異步操做,一旦執行結束,調用 completion handler,最後用一個過濾器指明須要被刪除的行。

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

如今打開 NoteListViewController.swift,定義下一個方法 UITableViewDataSource

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

把上面的方法添加到代碼中以後,每次你左滑一行筆記,右邊會出現默認的 Delete 按鈕。並且,當用戶點擊 Delete 按鈕時,會執行 if 後面對應的代碼,以下:

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

首先,找到所選中行對應的對象,而後,調用 Note 類裏的新方法進行刪除,若是刪除成功,從 notes 數組裏移除 Note 對象,從新加載 tableview,更新 UI 顯示內容。

就是這麼簡單!

那麼,如何排序呢?

你可能正在想,如何對讀取出來的數據進行排序。排序很是有用,能夠基於一個或者多個字段進行升序或降序排列,最後改變返回數據的順序。例如,咱們能夠將咱們全部的筆記按照修改日期的前後進行排序。

不幸的是,在我寫這篇教程時,SwiftyDB 還不支持對數據進行排序,這確實是個劣勢,不過還有一個解決辦法:手動排序。爲了演示手動排序的方法,咱們在 NoteListViewController.swift 文件裏建立最後一個方法 sortNotes()。這裏會使用 Swift 自帶的 sort() 函數:

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

因爲咱們沒法直接比較 NSDate 對象,咱們先轉換成時間戳(double 類型的值)。接着執行比較,返回比較的結果。上面的代碼讓咱們進行筆記排序,最新修改的筆記排在 notes 數組最前面。

只要 notes 數組發生了改變,上面的方法就要被調用。咱們先更新 loadNotes 方法,以下:

func loadNotes() {
    note.loadAllNotes { (notes) -> Void in
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
            if notes != nil {
                self.notes = notes
                
                self.sortNotes()  // 添加此行代碼對全部的筆記進行排序
                
                self.tblNotes.reloadData()
            }
        })
    }
}

接着在下方的兩個 delegate 方法裏作一樣的事情:

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

                self.sortNotes() // 添加此行代碼對全部的筆記進行排序
                
                self.tblNotes.reloadData()
            }
        })
    }
}


func didUpdateNote(noteID: Int) {
    ...
    
    if indexOfEditedNote != nil {
        note.loadSingleNoteWithID(noteID, completionHandler: { (note) -> Void in
            if note != nil {
                self.notes[indexOfEditedNote] = note
                
                self.sortNotes()  // 添加此行代碼對全部的筆記進行排序
                
                self.tblNotes.reloadData()
            }
        })
    }
}

如今再運行 App,全部的筆記都會按照它們的修改時間順序顯示。

總結

毫無疑問,SwiftyDB 是很是棒的工具,能夠用在各類應用裏。很是簡單、高效且可靠,當咱們的應用必須使用數據庫時,SwiftyDB 能夠知足各類需求。在本文的 demo 輔導教程裏,咱們瞭解了 SwiftyDB 的基本知識,還有不少東西等待你去學習。固然,如需更多幫助,這裏有官方文檔供你查閱。在今天的例子講解中,爲了方便編寫輔導教程,咱們建立的這個數據庫有一個表對應 Note 類。在實際開發中,你想建立多少表就能建立多少表,只要有對應的 model 代碼便可(對應的類)。就我我的而言,我確定會在個人項目中使用 SwiftyDB 的,實際上,我正在這樣作。如今你已經瞭解了 SwiftyDB,你也見識了它如何工做的,如何實現的。SwiftyDB 可否成爲你工具箱裏的新成員,徹底由你決定。總之,我但願閱讀這篇文章並非在浪費你的時間,但願你也學到了一些新的知識,在咱們下一教程出來以前,祝您開心!

僅供參考,你能夠在 GitHub 上下載完整的工程

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索