用 SwiftyDB 管理 SQLite 數據庫

作者:AppCoda秋茫,原文鏈接史简,原文日期:2016-03-16
譯者:Crystal Sun;校對:numbbbbb肛著;定稿:shanks

選擇哪種數據持久化的方式圆兵,是我們在開發(fā) App 時常常遇到的問題跺讯。我們有太多選擇了:創(chuàng)建一個單獨的文件、使用 CoreData 或者創(chuàng)建 SQLite 數據庫衙傀。使用 SQLite 數據庫有點麻煩抬吟,因為首先要先創(chuàng)建數據庫,提前寫好表和字段统抬。此外火本,從編程的角度來看,數據的存儲聪建、更新钙畔、和獲取都不是很容易的操作。

而當我們使用 GitHub 上的 SwiftyDB 這個第三方庫時金麸,上面的這些問題都可以輕而易舉地解決擎析。SwiftyDB,用作者的話來說挥下,就是即插即用型的好幫手递雀。SwiftyDB 將開發(fā)者從繁重的手動創(chuàng)建 SQLite 數據庫的工作中解放出來欺缘,再也不用提前定義好各種表和字段了贤笆。SwiftyDB 中類的屬性能夠自動完成上述工作府树,可以直接用類作為數據模型。除此之外偎蘸,所有對數據庫的操作都被封裝起來庄蹋,開發(fā)者可以把所有的注意力放到應用的邏輯層面上。簡單強悍的 API 可以讓處理數據成為小菜一碟的事情迷雪。

不過需要強調一下限书,SwiftyDB 并不能創(chuàng)造奇跡。它只是一個靠譜的第三方庫章咧,可以很好地完成它該做的事情(雖然有一些特性目前還不具備)倦西。盡管如此,它仍然是一個非常好用的工具赁严,值得你花時間學習扰柠。在本篇文章中,我們將學習 SwiftyDB 的基本使用操作误澳。

可以從這里找到文檔,看完這篇文章后最好再去看看文檔秦躯。如果你一直想用 SQLite忆谓,可是從來沒有真正開始,那 SwiftyDB 是一個好的開始踱承。

好了倡缠,讓我們開始探索這個全新的哨免、令人期待的工具吧。

關于 Demo App

在這篇文章中昙沦,我們要創(chuàng)建一個非常簡單的筆記應用琢唾,可以實現(xiàn)如下這些基本操作:

  • 列出筆記
  • 創(chuàng)建新的筆記
  • 更新已經創(chuàng)建的筆記的內容
  • 刪除筆記

很明顯,SwiftyDB 將要管理一個 SQLite 數據庫盾饮,上面列出的操作足以向你展示如何使用 SwiftyDB采桃。

簡單起見,我事先創(chuàng)建了一個工程丘损,點擊下載然后打開工程普办。用 Xcode 打開工程后,能夠看到所有的基本功能徘钥,不過缺少與數據有關的代碼衔蹲。運行項目,你就能看到全貌了呈础。

應用有一個導航欄舆驶,在第一個 view controller 中,有一個 tableview 列出所有筆記而钞。

<center>



</center>

點擊某個筆記沙廉,我們可以編輯更新內容,如果向左滑動某條筆記笨忌,可以刪除筆記:

<center>



</center>

創(chuàng)建一個新筆記只需點擊導航欄上的加號按鈕蓝仲,下面是我們在編輯筆記時可以進行的操作:

  1. 設置筆記的標題和內容。
  2. 更改字體官疲。
  3. 更改字體的大小袱结。
  4. 更改字體的顏色。
  5. 添加圖片途凫。
  6. 移動圖片到另外一個位置垢夹。

上述所有值的改變都會存儲到數據庫中。在最后兩條中维费,圖片實際上是存儲在應用的 documents directory 中果元,我們在數據庫中只是存儲圖片的名字和 frame。此外犀盟,我們還要創(chuàng)建一個類來管理圖片(更多細節(jié)參見后面的內容)而晒。

<center>



</center>

最后還要強調一點,雖然你只是下載了一個簡單的項目阅畴,但是在下一節(jié)中它會變成一個 workspace倡怎,因為我們要使用 CocoaPods 來下載 SwiftyDB 以及其他依賴項目。

準備好了嗎?如果你在 Xcode 中打開了剛剛下載的初始工程监署,那么請先關閉颤专。

安裝 SwiftyDB

第一件事情就是下載 SwiftyDB,然后在工程中使用钠乏。下載庫的文件然后放到工程中可不管用栖秕,我們要先安裝 CocoaPods。安裝過程不復雜晓避,不會花費太多時間簇捍,即使你從來沒有用過 CocoaPods。詳細內容請點擊鏈接够滑。

安裝 CocoaPods

我們要將 CocoaPods 安裝到系統(tǒng)中垦写,如果你已經安裝了 CocoaPods,那么請?zhí)^這一步彰触,如果沒有梯投,那么打開 Terminal 終端 ,輸入下列命令:

sudo gem install cocoapods

然后按回車况毅,輸入 Mac 密碼分蓖,等一會然后開始下載,下載完畢后不要關閉 Terminal 終端 尔许,我們之后還會用到么鹤。

安裝 SwiftyDB 和其他的依賴庫

使用 cd 命令找到初始工程對應的文件夾(仍然是在 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 這個軟件)恨憎,然后將內容修改成:

use_frameworks!

target 'NotesDB' do
    pod "SwiftyDB"
end

<center>



</center>

這行代碼實際上就做了 pod "swiftyDB" 一件事。CocoaPods 會下載 SwiftyDB 庫和所有的依賴庫郊楣,還會創(chuàng)建一些新的子文件夾憔恳,以及一個 Xcode workspace。

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

pod install

<center>



</center>

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

<center>



</center>

開始使用 SwiftyDB - 我們的 Model

NotesDB 工程中作烟,有個文件叫做 Note.swift,目前還是空的砾医。這就是我們今天要講述的重點內容拿撩,我們要創(chuàng)建一些類,表示一條筆記的實體如蚜,在理論層面上压恒,即將完成的工作就是 iOS MVC 模式里的 Model

首先需要引入 SwiftyDB 庫错邦,在文件的頭部輸入如下代碼:

import SwiftyDB

現(xiàn)在探赫,聲明最重要的一個類:

class Note: NSObject, Storable {

}

我們使用 SwiftyDB 時,需要遵循幾條規(guī)則撬呢,上面這個類的第一行體現(xiàn)出其中兩條:

  1. 帶有屬性的類如果要用 SwiftyDB 存到數據庫伦吠,必須是 NSObject 類的子類
  2. 帶有屬性的類如果要用 SwiftyDB 存到數據庫,必須必須遵守 Storable 協(xié)議(也是一個 SwiftyDB 協(xié)議)魂拦。

現(xiàn)在毛仪,我們要想一想,這個類需要哪些屬性芯勘,這就需要了解 SwiftyDB 的一條新規(guī)則:從數據庫中獲取數據時箱靴,datatypes 屬性必須是這里列出的一種,以便能載入整個 Note 對象荷愕,而不是簡單數據(比如一個都是字典的數組)衡怀。如果有某個屬性是“不兼容的”數據類型,那么我們就需要額外做一些操作安疗,把它們轉換成建議的類型(我們在之后會進行詳細的說明)抛杨。默認情況下,將數據存儲到數據庫時茂契,不兼容的數據類型的數據都會被 SwiftyDB 直接忽略掉蝶桶。也不會創(chuàng)建對應的表單。同樣的掉冶,對于我們不想存儲到數據庫中的其他屬性真竖,我們也會特殊對待的。

目前需要說明的最后一條要求:遵守 Storable 協(xié)議的類必須執(zhí)行 init 方法:

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

現(xiàn)在厌小,我們已經有所需的信息了恢共,下面開始聲明類的屬性吧。有些屬性后面才需要用到璧亚,這里先聲明好:

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!
    
    ...
}

除了第一個之外讨韭,其他的無需多言。對象初始化后(如果數據庫不存在)會創(chuàng)建一個新的數據庫(名為 notes.sqlite)并自動創(chuàng)建一個表,表單會和擁有正確數據類型的屬性匹配透硝。反之狰闪,如果數據庫已經存在了,就會直接打開數據庫濒生。

你可能會注意到埋泵,上面的屬性都是描述一條筆記和我們想存儲的特性(標題、問題罪治、文字顏色丽声、字體和大小、創(chuàng)建和修改日期)觉义,但是唯獨沒有筆記中存儲的圖片雁社。哈哈,我是故意的晒骇,我要給圖片單獨創(chuàng)建一個類霉撵,只存儲兩個屬性:圖片的名字和尺寸。

所以洪囤,繼續(xù)在 Note.swift 文件中創(chuàng)建下列類喊巍,放到之前的那個類的上方或者下方皆可:

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

注意,在類中箍鼓,圖片的 frame 是一個 NSData 對象崭参,不是 CGRect 對象。必須這樣操作款咖,因為這樣我們可以非常容易的將值存儲到數據庫里何暮。過一會你就會看到我們是如何轉換的,到時候你就明白為什么我們要使用 NSCoding 協(xié)議铐殃。

回到 Note 類海洼,我們聲明一個 ImageDescriptor 數組,如下文:

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

這里有一個限制富腊,現(xiàn)在是時候提到它了坏逢,就是實際上 SwiftyDB 不會把集合存儲到數據庫中。簡單來說赘被,我們的 images 數組永遠不會被存儲到數據庫里是整,我們不得不解決圖片的存儲問題。我們可以使用受支持的數據類型中的一個(看我之前提供的連接)民假,而最合適的數據類型是 NSData浮入。所以,我們不會把 images 數組存儲到數據庫里羊异,而是存儲下列新的屬性:

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

但是我們如何才能將帶有 ImageDescriptor 對象的 images 數組變成 imagesData``NSData 對象呢事秀?恩彤断,答案就是 歸檔(archiving) 這個 images 數組,使用 NSKeyedArchiver 類生成 NSData 對象易迹。我們在后面會演示如何用代碼實現(xiàn)宰衙,這里只是介紹一下實現(xiàn)思路,后面再來修改 ImageDescriptor 類睹欲。

如你所知菩浙,一個類可以被歸檔(在其他編程語言中也就做 序列化(serialized)),只要類的所有屬性都可以被序列化就行句伶。在我們的例子中,這是可行的陆淀,因為ImageDescriptor 類里的這兩個屬性的數據類型(NSDataString)是可以被序列化的考余。然而這還不夠,因為我們還必須要 編碼(encode)解碼(decode) 它們轧苫,以便于歸檔和解壓(unarchive)楚堤,這也就是我們需要 NSCoding 協(xié)議的原因。有了 NSCoding 協(xié)議含懊,我們可以引進如下方法(其中一個就是 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 協(xié)議和 NSKeyedArchiver 類的信息請參見這里這里,我們不會在這里討論岔乔。

除此之外酥筝,我們定義一個便利的自定義的 init 方法。代碼非常簡單雏门,一看就懂:

class ImageDescriptor: NSObject, NSCoding {
    ...
        
    init(frameData: NSData!, imageName: String!) {
        super.init()
        self.frameData = frameData
        self.imageName = imageName
    }   
}

在這一節(jié)中我們快速介紹了 SwiftyDB 庫嘿歌。雖然我們還沒有大量使用 SwiftyDB,但是這部分很重要茁影,因為它包含三個要點:

  1. 創(chuàng)建一個能使用 SwiftyDB 庫的類宙帝。
  2. 了解一些在使用 SwiftyDB 庫時的規(guī)則。
  3. 了解一些有關數據類型的限制要求募闲,哪些數據類型可以被存儲到 SwiftyDB里步脓。

注意:如果你在 Xcode 中看到錯誤提示,立即 Build 工程(Command + B)浩螺,錯誤提示就會消失了靴患。

主鍵和忽略屬性

在和數據庫打交道時,強烈推薦使用 主鍵(primary keys)要出,它們能夠幫你在數據庫表中創(chuàng)建獨一無二的標識符蚁廓,進行各種各樣的操作(例如,更新某個數據)厨幻。你可以在這里找到有關主鍵的定義相嵌。

在 SwiftyDB 數據庫中腿时,將類中的某個或某些屬性定義為主鍵的操作非常簡單,庫里提供了 PrimaryKeys 協(xié)議饭宾,所有類都應該實現(xiàn)這個協(xié)議批糟,從而讓對應的表中有主鍵,這樣對象才能有獨一無二的標識符看铆。實現(xiàn)方法非常簡單徽鼎,動手吧。

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

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

在我們的 demo 里审残,我想讓 noteID 屬性成為 sqlite 數據庫對應的表里唯一的主鍵。如果需要更多的主鍵石抡,用逗號分隔即可(比如,return ["key1","key2","key3"])助泽。

除此之外啰扛,并不是類中所有的屬性都要存儲到數據庫中,你應該明確指出哪些不存儲嗡贺。例如隐解,在 Note 類中,我們有兩個屬性是不存儲到數據庫里的(要么就是不能被存儲诫睬,要么就是我們不想存儲):images 數組和 database 對象煞茫。我們如何明確地排除這兩個屬性呢?引入 SwiftyDB 提供的另外一個協(xié)議:IgnoredPropertie

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

如果還有更多屬性我們不想存儲到數據庫中摄凡,那么也需要添加到上面的代碼中溜嗜,例如,假設我們有這么一個屬性:

var noteAuthor: String!

我們不想把它存儲到數據庫中架谎,這就需要把這個屬性添加到 IgnoredProperties 協(xié)議里:

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

保存一個新筆記

我們在 Note 里已經做了很多工作炸宵,是時候回到 demo app 的功能了。我們還沒有給新的類添加任何方法呢谷扣,接下來就做這件事土全,補全所有缺失的功能。

首先要有筆記会涎,需要告訴 App 如何正確地使用 SwiftyDB 來保存筆記和兩個新創(chuàng)建的類裹匙。大部分的操作會在 EditNoteViewController.swift 中實現(xiàn),打開此文件末秃,在寫代碼之前概页,我先列出幾條特別重要的屬性:

  • imageViews:這個數組里有所有的 image view 對象,對象里有所有添加到筆記的圖片练慕。這個數組已經存在了惰匙,過會就能發(fā)現(xiàn)它的強大作用技掏。
  • currentFontName:里面有應用于文本的字體名字。
  • currentFontSize:里面是文本的字體的字號项鬼。
  • editedNoteID:即將更新內容的筆記的 noteID 值(primary key)哑梳。一會兒我們就會用到。

基礎的功能已經在初始工程中提前寫好了绘盟,我們需要做的就是補全缺失的 saveNote() 方法中的邏輯鸠真。首先做兩件事情:一、如果筆記沒有標題或者筆記沒有內容龄毡,那么吠卷,不允許用戶保存筆記。二沦零、在保存筆記時祭隔,隱藏鍵盤。如下:

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 類型的數字作為主鍵。你可以創(chuàng)建生成任何你想要的值丈牢,只要它們是獨一無二的祭钉。在這里,我們把當前時間戳作為我們的主鍵己沛,不過在實際的應用開發(fā)中這不是一個好主意慌核,因為時間戳包含了太多數字。然而對我們目前的這個應用來說申尼,時間戳還是一個不錯的選擇垮卓,畢竟這是創(chuàng)建獨一無二數值最簡單的方法。

  • 當我們第一次存儲一條新筆記時师幕,把當前時間(也就是 NSDate 對象)設置為創(chuàng)建日期和修改日期粟按。

  • 這里唯一需要特殊處理的行為是將文本顏色轉換成 NSData 對象,通過使用 NSKeyedArchiver 類來存儲顏色對象霹粥。

接下來看如何存儲圖片灭将。我們創(chuàng)建一個新的方法來處理圖片數組。這個方法主要做兩件事:將實際圖片存儲到應用的 documents 目錄下后控,給每個圖片創(chuàng)建 ImageDescriptor 對象并添加到 images 數組里庙曙。

在實現(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())
    }
}

上面這個方法到底做了什么呢:

  1. 首先捌朴,我們確認 images 數組是否存在吴攒。如果為空,進行初始化男旗,如果存在舶斧,我們只需要將里面的數據清除即可,在更新既有的筆記時察皇,第二個方法在會非常有用茴厉。
  2. 然后對每個圖片我們創(chuàng)建一個獨一無二的名字,每個名字都類似這樣:“img_12345679_1”什荣。
  3. 使用 init 方法初始化一個新的 ImageDescriptor 方法矾缓, image view 的 frame 和名字是該方法的參數。toNSData() 方法已經實現(xiàn)好了稻爬,是 CGRect 的擴展嗜闻,你可以從 Extensions.swift 文件里找到。目的是將 frame 轉換成 NSData 對象桅锄。一旦新的 ImageDescriptor 對象準備好了琉雳,就可以添加到 images 數組里了。
  4. 我們將實際的圖片存儲到 documents 目錄下友瘤,saveImage(_: withName:) 類方法可以在 Helper.swift 文件里找到翠肘,這里還有很多有用的類方法。
  5. 最后辫秧,當所有的 image views 都處理過后束倍,通過 archiving(歸檔)我們將 images 數組轉換成 NSData 對象,存儲到 imagesData 屬性里盟戏。上面代碼中的最后一行绪妹,是 NSCoding 協(xié)議必須實現(xiàn)的方法。

上面的 else 看起來似乎有些多余柿究,實際上很有用邮旷。默認情況下,imagesData 為空蝇摸,如果某條筆記里沒有添加圖片廊移,就會一直為空直。然而探入,SQLite 不識別 nil(空)狡孔,SQLite 理解的是 NSNull,也就是轉換成 NSData 對象蜂嗽。

回到 EditNoteViewController.swift 文件中苗膝,用上我們剛剛創(chuàng)建的方法:

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

現(xiàn)在回到 Note.swift,實現(xiàn)實際存儲到數據庫的方法植旧。這里有個重點:SwiftyDB 可以同步或異步執(zhí)行任何數據庫相關操作辱揭,選擇哪種方法取決于應用的性質离唐。然而,我建議使用異步方法问窃,這樣在進行數據庫操作時亥鬓,不會阻塞主線程,也不會出現(xiàn) UI 控件突然卡住這種不好的用戶體驗域庇。不過我還是再強調一次嵌戈,選擇哪種方法,完全由你決定听皿。

這里我們用異步方式來存儲數據熟呛。如你所見,每個 SwiftyDB 方法都包含一個閉包尉姨,可以返回執(zhí)行結果庵朝。你可以在這里閱讀相關的信息,實際上又厉,我建議你現(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 設置為布爾值侄旬,作為該方法的參數,然后根據 asyncDataObject 的值來判斷是否創(chuàng)建一個新的筆記篷朵,或者更新一個已存在的筆記勾怒。

此外婆排,第二個參數是 completion handler声旺。能否用合適的參數值調用它,取決于我們的存儲是否成功段只。當你的任務在后臺使用異步方法時腮猖,我建議你使用 completion handler。這樣赞枕,當任務完成后澈缺,你就能通知調用方法,將任何結果或者數據調回來炕婶。

上面你看到的這些姐赡,其他的數據庫相關方法中也有。我們會先檢查錯誤柠掂,然后根據是否存在結果來執(zhí)行下一步的操作项滑。在上面的例子中,如果出現(xiàn)錯誤涯贞,我們就可以調用 completion handler枪狂,傳入 false 值危喉,意味著存儲失敗,反之州疾,我們傳入 true 值辜限,表示操作成功。

回到 EditNoteViewController 類严蓖,完成 saveNote() 方法薄嫡。調用上面創(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)在的稿黍,那么存儲筆記功能已經可以正常使用了。

下載和列出筆記

創(chuàng)建和存儲新筆記的功能已經實現(xiàn)了崩哩,我們可以繼續(xù)開發(fā)讀取筆記功能了巡球。讀取筆記意味著將筆記列在 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 里執(zhí)行讀取功能的方法是 asyncObjectsForType(...),是一個異步執(zhí)行的方法汹押。結果要么是一個錯誤矿筝,要么就是從數據庫里讀取一個 note 對象集合(數組)。在第一種情況下棚贾,我們調用 completion handler 傳入 nil窖维,告訴調用者這里在讀取數據時遇到了問題。在第二種情況下妙痹,把 'Note' 對象傳入 completion handler铸史,這樣可以在方法之外使用它們。

現(xiàn)在回到 NoteListViewController.swift 文件怯伊,首先必須聲明一個數組包含 Note 對象(剛剛從數據庫中讀取出來)琳轿。這個數組就是 tableview 的 datasource(很明顯嘛)。所以,在類的開頭利赋,加入下列代碼:

var notes = [Note]()

除此之外水评,初始化一個新的 Note 對象,可以使用之前創(chuàng)建的 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 中,具體說來橙依,我們會展示筆記的標題证舟、創(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)在運行應用吧窗骑,你創(chuàng)建的所有筆記都會出現(xiàn)在 tableview 中了女责。

另外一種獲取數據的方法

現(xiàn)在我們是用 asyncObjectsForType(...) 方法來加載數據庫中所有的筆記。如你所知慧域,這個方法會返回一個數組對象(在我們的例子里鲤竹,就是 Note 對象)浪读,我覺得這個方法特別有用昔榴,但并不能適應所有情況。某些情況下碘橘,讀取實際的數值數據會更方便互订。

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

你可以在這里這里找到更多的信息吐葵,我把這個任務留給你规揪,作為一個練習:修改 Note 類,加載簡單的數據和數值温峭,而不是只加載對象猛铅。

更新一條筆記

我們還想讓應用具有編輯筆記的功能,換句話說凤藏,當用戶點擊某一行時奸忽,我們就顯示 EditNoteViewController 界面,其中包含這條筆記的所有信息揖庄;用戶修改之后保存栗菜,我們需要存儲筆記修改后的信息。

首先蹄梢,在 NoteListViewController.swift 文件里疙筹,我們需要一個新的屬性來存儲所選筆記的 ID,所以我們在類的頂部寫入下列代碼:

var idOfNoteToEdit: Int!

下面我們來實現(xiàn)一個 UITableViewDelegate 方法禁炒,根據所有的行找到對應的 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
            }
        }
    }
}

到這里我們已經完成了一半的工作了凹蜂,在我們回到 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 方法來對返回的結果進行過濾锰扶。使用 Filter 類里的 equal(...) 方法可以設置我們想要的過濾條件棚潦。別忘了看一下這個鏈接栖茉,里面有更多實現(xiàn)過濾的方法(在從數據庫里取數據或者對象時)症汹。

通過上面的過濾方法踢故,我們實際上可以讓 SwiftyDB 只加載符合條件的筆記:上面方法中參數的值對應的 noteID 的筆記。當然七冲,只會返回一條筆記痛倚,因為我們這里使用的是主鍵,一個主鍵只對應一個記錄澜躺。

返回的結果會作為 Note 對象的數組蝉稳,所以需要先獲取集合的第一個(唯一一個)元素。然后掘鄙,必須將 image data(如果存在的話)轉換為 ImageDescriptor 對象數組耘戚,然后將其賦值給 images 屬性。這點很重要操漠,如果跳過這一步收津,下載下來的筆記里的圖片都無法顯示。最后浊伙,根據是否成功取得筆記數據撞秋,我們調用 completion handler。如果成功取得筆記吧黄,我們把讀取來的對象傳給 completion handler部服,讓調用者使用唆姐,如果沒有成功取得筆記拗慨,返回 nil,因為沒有取得對象。

現(xiàn)在赵抢,回到 EditNoteViewController.swift 文件剧蹂,聲明并初始化一個新的 Note 屬性:

var editedNote = Note()

這個對象首先調用上面實現(xiàn)的新方法,然后存儲從數據庫中加載的數據烦却。

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

在下面的代碼中你會看到摩渺,loadSingleNotedWithID(...) 會在 completion handler 獲取到筆記之后給所有屬性賦值简烤。也就是說,我們會設置筆記的標題摇幻、內容横侦、文字顏色、文字字體等等绰姻。不僅如此枉侧,如果筆記里有圖片,我們還會給每條筆記創(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() 方法,這樣當一條已有筆記更新內容后黍特,不會創(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é)中闻妓,我們會修復這個問題。

你可能已經猜到了掠械,我們會使用 代理模式(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)建新筆記或者編輯已有筆后,對應的 delegate 方法就會被調用镜会。目前我們只完成了一半的工作炉抒,讓我們回到 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()
            }
        })
    }
}

如你所見季率,我們從數據庫里獲取 noteID 參數值對應的對象野瘦,然后(如果對象存在)我們把對象添加到 notes 數組,重新加載 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,找到之后從數據庫里加載對應的筆記泞遗,用新的對象替換舊的對象惰许,然后更新 tableview,新的修改日期就會出現(xiàn)了史辙。

刪除記錄

還有最后一個主要的功能沒有開發(fā)汹买,那就是刪除筆記。很明顯聊倔,我們需要在 Note 類里實現(xiàn)我們最后一個方法晦毙,每次想刪除筆記時都會調用這個方法。請打開 Note.swift 文件耙蔑。

這里唯一的一個知識點就是 SwiftyDB 方法會從數據庫里直接刪除數據见妒,在接下來的實現(xiàn)方法中你會看到這一點。和以前一樣甸陌,這個操作還是異步操作须揣,一旦執(zhí)行結束盐股,調用 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 后面對應的代碼,如下:

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 文件里創(chuàng)建最后一個方法 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 類型的值)。接著執(zhí)行比較倍谜,返回比較的結果迈螟。上面的代碼讓我們進行筆記排序叉抡,最新修改的筆記排在 notes 數組最前面。

只要 notes 數組發(fā)生了改變答毫,上面的方法就要被調用褥民。我們先更新 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洗搂,所有的筆記都會按照它們的修改時間順序顯示消返。

總結

毫無疑問,SwiftyDB 是非常棒的工具耘拇,可以用在各種應用里撵颊。非常簡單、高效且可靠惫叛,當我們的應用必須使用數據庫時倡勇,SwiftyDB 可以滿足各種需求。在本文的 demo 輔導教程里嘉涌,我們了解了 SwiftyDB 的基本知識妻熊,還有很多東西等待你去學習。當然仑最,如需更多幫助扔役,這里有官方文檔供你查閱。在今天的例子講解中词身,為了方便編寫輔導教程厅目,我們創(chuàng)建的這個數據庫有一個表對應 Note 類。在實際開發(fā)中法严,你想創(chuàng)建多少表就能創(chuàng)建多少表损敷,只要有對應的 model 代碼即可(對應的類)。就我個人而言深啤,我肯定會在我的項目中使用 SwiftyDB 的拗馒,實際上,我正在這樣做∷萁郑現(xiàn)在你已經了解了 SwiftyDB诱桂,你也見識了它如何工作的,如何實現(xiàn)的呈昔。SwiftyDB 能否成為你工具箱里的新成員挥等,完全由你決定〉涛玻總之肝劲,我希望閱讀這篇文章并不是在浪費你的時間,希望你也學到了一些新的知識,在我們下一教程出來之前辞槐,祝您開心掷漱!

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

本文由 SwiftGG 翻譯組翻譯榄檬,已經獲得作者翻譯授權卜范,最新文章請訪問 http://swift.gg

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末鹿榜,一起剝皮案震驚了整個濱河市海雪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌犬缨,老刑警劉巖喳魏,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棉浸,死亡現(xiàn)場離奇詭異怀薛,居然都是意外死亡,警方通過查閱死者的電腦和手機迷郑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門枝恋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嗡害,你說我怎么就攤上這事焚碌。” “怎么了霸妹?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵十电,是天一觀的道長。 經常有香客問我叹螟,道長鹃骂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任罢绽,我火速辦了婚禮畏线,結果婚禮上,老公的妹妹穿的比我還像新娘良价。我一直安慰自己寝殴,他們只是感情好,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布明垢。 她就那樣靜靜地躺著蚣常,像睡著了一般。 火紅的嫁衣襯著肌膚如雪痊银。 梳的紋絲不亂的頭發(fā)上抵蚊,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機與錄音,去河邊找鬼泌射。 笑死粘姜,一個胖子當著我的面吹牛,可吹牛的內容都是我干的熔酷。 我是一名探鬼主播孤紧,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拒秘!你這毒婦竟也來了号显?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤躺酒,失蹤者是張志新(化名)和其女友劉穎押蚤,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體羹应,經...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡揽碘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了园匹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雳刺。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖裸违,靈堂內的尸體忽然破棺而出掖桦,到底是詐尸還是另有隱情,我是刑警寧澤供汛,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布枪汪,位于F島的核電站,受9級特大地震影響怔昨,放射性物質發(fā)生泄漏雀久。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一朱监、第九天 我趴在偏房一處隱蔽的房頂上張望岸啡。 院中可真熱鬧,春花似錦赫编、人聲如沸巡蘸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悦荒。三九已至,卻和暖如春嘹吨,著一層夾襖步出監(jiān)牢的瞬間搬味,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碰纬,地道東北人萍聊。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像悦析,于是被迫代替她去往敵國和親寿桨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內容