使用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,事實上我已經打算這麼幹了。在任何狀況下,你如今已經知道了它是如何工做的以及如何進行調用操做。能不能在你的工具箱裏面再加上這個徹底取決於你本身。不論怎樣,我但願你閱讀這篇文章的時間沒有被浪費,而且你學到了一些新的知識或者更低。在下一篇教程到來以前一切如意吧!

做爲參考,你能夠在這裏下載到完整的工程。

相關文章
相關標籤/搜索