學習如何使用 Realm 數據庫引擎來輕松地實現 Swift 的數據存儲
Realm 是一個跨平台的移動數據庫引擎,於 2014 年 7 月發布,准確來說,它是專門為移動應用所設計的數據持久化解決方案之一。
Realm 可以輕松地移植到您的項目當中,並且絕大部分常用的功能(比如說插入、查詢等等)都可以用一行簡單的代碼輕松完成!
Realm 並不是對 Core Data 的簡單封裝,相反地, Realm 並不是基於 Core Data ,也不是基於 SQLite 所構建的。它擁有自己的數據庫存儲引擎,可以高效且快速地完成數據庫的構建操作。
之前我們提到過,由於 Realm 使用的是自己的引擎,因此, Realm 就可以在 iOS 和 Android 平台上共同使用(完全無縫),並且支持 Swift 、 Objective-C 以及 Java 語言來編寫( Android 平台和 iOS 平台使用不同的 SDK )。
數以萬計的使用 Realm 的開發者都會發現,使用 Realm 比使用 SQLite 以及 Core Data 要快很多。下面我們給出一個例子,分別展示 Core Data 和 Realm 在執行一個斷言查詢請求並且排序結果所使用的代碼量:
// Core Data let fetchRequest = NSFetchRequest(entityName: "Specimen") let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString) fetchRequest.predicate = predicate let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) fetchRequest.sortDescriptors = [sortDescriptor] let error = NSError() let results = managedObjectContext?.executeFetchRequest(fetchRequest, error:&error)
而換成了 Realm 呢?您會驚歎於 Realm 的簡單的:
// Realm let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString); let specimens = Specimen.objectsWithPredicate(predicate).arraySortedByProperty("name", ascending: true)
使用 Realm 可以讓代碼變得十分簡潔,從而讓您的代碼易讀易寫。
綜上所述,我們之所以使用 Realm 的理由不外乎如下幾點:
跨平台 :現在絕大多數的應用開發並不僅僅只在 iOS 平台上進行開發,還要兼顧到 Android 平台的開發。為兩個平台設計不同的數據庫是愚蠢的,而使用 Realm 數據庫, iOS 和 Android 無需考慮內部數據的架構,調用 Realm 提供的 API 就可以完成數據的交換,實現 “ 一個數據庫,兩個平台無縫銜接 ” 。
簡單易用 : Core Data 和 SQLite 冗余、繁雜的知識和代碼足以嚇退絕大多數剛入門的開發者,而換用 Realm ,則可以極大地減少學習代價和學習時間,讓應用及早用上數據存儲功能。
可視化 : Realm 還提供了一個輕量級的數據庫查看工具,借助這個工具,開發者可以查看數據庫當中的內容,執行簡單的插入和刪除數據的操作。畢竟,很多時候,開發者使用數據庫的理由是因為要提供一些所謂的 “ 知識庫 ” 。
本教程將會向您介紹 Realm 在 iOS 平台上的簡單應用,即導入 Realm 框架、創建數據模型、執行查詢以及插入、更新和刪除記錄,以及使用既有的數據庫。
提示:原文教程寫於 2014 年,而 Realm 的版本更新得十分快,因此,本教程並不會拘泥於原文教程所述內容,而是根據 Realm 的版本更新進行相關修改。
原文作者提到,要在 Realm 抵達 1.0 版本的時候再來更新這篇教程,大家盡請期待吧!
讓我們開始吧
我們將會以一個實際的項目來進行教程:假設您在西雙版納自然保護區覓得了一份職位 “ 監測員 ” ,職責是記錄這個 “ 動植物王國 ” 當中所發現物種的相關信息,包括種群數量、發現區域、年齡結構等等。因此,您需要一個助手來幫忙記錄您的發現,但是很可惜的是,保護區並沒有多余的人手來做您的助手(主要是沒錢)。所以沒有辦法,我們必須為自己制作一個虛擬的 “ 助手 ” ,也就是一個以 “ 物種監測 ” 命名的 APP ,這樣就可以隨手記錄我們的發現了!
點擊此處下載本教程所使用的起始項目
在 Xcode 當中打開我們的起始項目。此時, MapKit 已經在項目當中建立好了,而且項目已經擁有了一些簡單的創建、更新和刪除物種信息的功能。
提示:如果您對 MapKit 的相關知識感興趣,可以查看 Introduction to MapKit tutorial ,這篇教程將會深入闡述 MapKit 是如何工作的。
現在,讓我們前往 Realm 的官網去下載 Realm 的框架吧: http://static.realm.io/downloads/cocoa/latest
Realm 的使用需求如下:
iOS ≥ 7 或者 Mac OS X ≥ 10.9
Xcode ≥ 6
現在 Realm 的版本為: 0.91.5
解壓下載下來的 Realm 壓縮包。在壓縮包中,我們可以看到一個名為 iOS 的文件夾。打開這個文件夾,然後將 Realm.framework 文件拖入到我們的起始項目中,最好拖放到 “Frameworks” 文件夾中以確保文件有序(強迫症患者 ~ )。
將框架文件拖入到項目當中
之後,一定要確保勾選了 Copy Items if needed 選項,然後單擊 Finish 按鈕就完成了往項目中添加框架的操作。
之後,定位到項目設置中 SISpeciesNotes 的 General 選項卡,然後在 Link Binary with Libraries 欄目中添加 libc++.dylib 動態庫文件。
然後回到解壓的 Realm 文件夾中,打開名為 Swift 的文件夾,然後將裡面的 RLMSupport.swift 文件拖入到項目當中。這個文件包含了用於 Realm 相關類的 Swift 簡便方法,比如說 RLMResults 中的 Generator 方法,這樣就可以像使用原生數組一樣使用 Realm 數組了。
好的,我們的准備工作就完成了!您可以嘗試運行一下起始項目,以確保沒有任何錯誤產生。如果出現錯誤的話,請仔細查看上面所述的一些步驟,確保沒有任何疏漏發生。運行成功後的基本界面如下所示:
應用界面
Realm Browser 介紹
Realm 資源包中包含了一個很有用的實用工具,可以幫助我們更好地管理 Realm 數據庫,那就是 Realm Browser 。
Realm Browser 可以讓您輕松地讀寫 Realm 數據庫(以 .realm 結尾),因此我們無需頭疼如何去查看 Realm 專有數據庫的邏輯結構以及其中的數據,可視化的操作就如同 SQLite 的其他數據庫查看工具一樣,十分簡單、易用(雖然 Realm Browser 的功能還十分簡陋,真的只能讀寫而已)。
Realm Browser
Realm Browser 可以在解壓的 Realm 文件夾中的 browser 文件夾中找到。您也可以訪問 Realm GitHub repository 然後在其中的 tools/RealmBrowser 目錄中找到它。
您可以嘗試在 Realm Browser 中選擇 Tools -> Generate demo database 來試著探索一下 Realm Browser 的功能。
Realm 相關術語和主要類
為了幫助您更好地理解 Realm 的使用,下面我們將會對 Realm 的相關術語和主要類進行一個大致的介紹:
RLMRealm : RLMRealm 是框架的核心所在,是我們構建數據庫的訪問點,就如同 Core Data 的管理對象上下文( managed object context )一樣。出於簡單起見, realm 提供了一個名為 defaultRealm 的單例,在本教程中我們就僅使用這個單例來完成我們所需的功能。當然,我們也可以導入外部已經編寫好的 realm 數據庫文件,也可以在我們不需要將數據保存在硬盤上時使用 “ 內存實例對象 ” ( in-memory realm instance ),此外,還可以同時使用多個數據庫文件。
RLMObject :這是我們自定義的 realm 數據模型。創建數據模型的行為將會影響到數據庫的結構。要創建一個數據模型,我們只需要繼承 RLMObject ,然後設計我們想要存儲的屬性即可。
關系 (Relationships) :通過簡單地在數據模型中聲明一個 RLMObject 類型的屬性,我們就可以創建一個 “ 一對多 ” 的對象關系。同樣地,借助 RLMArray 我們還可以創建 “ 多對一 ” 和 “ 多對多 ” 的關系。
寫操作事務 (Write Transactions) :數據庫中的所有操作,比如創建、編輯,或者刪除對象,都必須在事務中完成。 “ 事務 ” 是指位於 beginWriteTransaction() 以及 commitWriteTransaction() 操作之間的代碼段。
查詢 (Queries) :要在數據庫中檢索信息,我們需要用到 “ 檢索 ” 操作。檢索最簡單的形式是對 RLMObject 對象發送 allObjects() 消息。如果需要檢索更復雜的數據,那麼還可以使用斷言( predicates )、復合查詢以及結果排序等等操作。
RLMResults :這個類是執行任何查詢請求後所返回的類,其中包含了一系列的 RLMObjects 對象。和 NSArray 類似,我們可以用下標語法來對其進行訪問,並且還可以決定它們之間的關系。不僅如此,它還擁有許多更強大的功能,包括排序、查找等等操作。
現在您應該對 Realm 有了一個大概的了解了,現在是時候來試著使用 Realm 來完成起始項目的剩余工作了。
創建第一個數據模型
好了,前面我們廢話了這麼多,現在終於要開始使用數據庫了。首先我們要創建一個數據模型,也相當於創建數據庫的一個 “ 表 ” 。
右鍵選擇 Xcode 項目導航器中的 Model 組,然後選擇 New File -> iOS -> Source -> Swift File ,創建一個新的 swift 文件,將其命名為 SpeciesModel 並且確保選中了 SISpeciesNotes 對象。
提示:您也許查看過 Realm 的開發文檔,它裡面介紹說可以使用 “ 插件 ” 來完成數據模型的簡單創建(也就是新建文件時,可以像新建 Core Data 數據模型文件一樣創建一個既定的模板數據模型),但是很遺憾的是,現在這個功能還只支持創建 OC 版本的數據模型文件,我們為了代碼的 “ 干淨 ” ,就不采用這種方法。
打開 SpeciesModel.swift 文件,然後用以下代碼替換文件中的內容:
import UIKit import Realm class SpeciesModel: RLMObject { dynamic var name = "" dynamic var speciesDescription = "" dynamic var latitude: Double = 0 dynamic var longitude: Double = 0 dynamic var created = NSDate() }
上面的代碼添加了一些屬性來存儲信息: name 屬性存儲物種名稱, speciesDescription 存儲物種的描述信息。對於 Realm 中的一些特定的數據類型,比如說字符串,必須要初始化。在本例中,我們使用空字符串來進行初始化。
latitude 以及 longitude 存儲了物種的經緯度信息。在這裡我們將其類型設置為 Double ( CLLocationDegrees 是 Double 的別名),並且使用 0 來進行初始化。
最後, created 存儲了這個物種所創建的時間信息。 NSDate() 將會返回當前時間,因此我們就用這個值來初始化這個屬性
好了,現在我們就成功創建了第一個 Realm 數據模型了,要不要動動腦來完成一個小小的挑戰呢?
我們知道,這些物種將會被劃分為不同的 “ 類別 ” ,您的任務就是自行創建一個 “ 類別 ” 數據模型,這個文件將被命名為 CategoryModel.swift ,然後這個新的數據模型只要一個字符串類型的屬性 ——name 。
以下是解決方案的代碼:
import UIKit import Realm class CategoryModel: RLMObject { dynamic var name = "" }
我們現在擁有了 CategoryModel 數據模型了,下面我們將通過某種方式將其與 SpeciesModel 數據模型關聯起來,搭建起 “ 關系 ” 。
重新回顧一下上一節的內容,我們可以通過簡單地聲明一個屬性來創建數據模型之間的關系。
打開 SpeciesModel.swift 文件,然後在 created 屬性下面添加如下語句:
dynamic var category = CategoryModel()
這個語句設置了 “ 物種 ” 和 “ 類別 ” 之間的 “ 一對多 ” 關系,這就意味著每個物種都只能夠擁有一個類別,但是一個類別可以從屬於多個物種。
好的,我們創建完了一個基礎數據模型了,現在是時候向數據庫中添加數據了!
添加數據
每當用戶添加了一個新的物種標記,用戶就可以對這個標記進行修改,比如說設置物種名字,選擇類別等等。打開 CategoriesTableViewController.swift 文件。這個視圖控制器將要在這個表視圖中顯示類別清單,以便用戶可以選擇。
因此,我們需要在應用初始運行時,給用戶提供幾個默認的類別以供選擇。
在類定義當中添加以下方法,別忘了在文件頂部導入 Realm 框架( import Realm ):
private func populateDefaultCategories() { self.results = CategoryModel.allObjects() // 1 if results.count == 0 { // 2 let realm = RLMRealm.defaultRealm() // 3 realm.beginWriteTransaction() // 4 let defaultCategories = Categories.allValues // 5 for category in defaultCategories { // 6 let newCategory = CategoryModel() newCategory.name = category realm.addObject(newCategory) } realm.commitWriteTransaction() // 7 self.results = CategoryModel.allObjects() } }
對應的標號注釋如下:
allobjects() 方法將會返回指定對象的所有元素,在本例中,我們向數據庫中的 CategoryModel 對象發送了一個查詢請求,返回這個表當中的所有行信息。注意的是,這裡我們得到的是一個 RLMResults 對象,這個對象用來存放我們的查詢結果。
如果查詢結果中的元素數量為 0 ,那麼就說明數據庫當中沒有類別信息的相關記錄,那麼就意味著這是用戶第一次啟動應用。
我們訪問默認的 realm 單例對象,然後將其用 realm 變量簡單表示,以供訪問
這一步將在默認 realm 數據庫中啟動一個事務 —— 現在,我們就可以向數據庫當中添加記錄了。
這裡我們使用已經定義過的 Categories 枚舉來創建一個含有全部默認類別的數組。
對於每個類別名稱來說,我們創建了一個對應的 CategoryModel 實例對象,然後設置其 name 屬性,最後將這個對象添加到 realm 當中。
當我們添加完所有的類別之後,調用 commitWriteTransaction() 方法來關閉事務,並且向數據庫提交數據。
只有調用了 commitWriteTransaction() 方法,我們之前做的所有關於事務的操作才能夠被成功運行,因為這涉及到 Realm 的內部處理的問題了。您可以像上面我們做的那樣,執行一些簡單的創建操作,或者您可以執行一些復雜的操作,比如說同時創建、更新、刪除多個對象等等。
然後在 viewDidLoad() 方法的底部加入以下代碼:
populateDefaultCategories()
這個方法將會在視圖加載的過程中,添加我們的測試用類別,並且執行向數據庫寫入數據的操作。
好了,現在我們的數據庫當中已經有了一些數據了,我們需要更新一下表試圖數據源相關方法,以顯示這些類別。找到 tableView(_:cellForRowAtIndexPath:) 方法,然後用以下代碼替換它:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("CategoryCell", forIndexPath: indexPath) as! UITableViewCell cell.textLabel?.text = (results[UInt(indexPath.row)] as! CategoryModel).name return cell }
這個聲明語句從 results 對象當中讀取對應行的名稱,然後設置到單元格的文本標簽上面顯示。
接下來,添加一個新的屬性:
var selectedCategory: CategoryModel!
我們用這個屬性來存儲當前選中的類別。
找到 tableView(_: willSelectedRowAtIndexPath:) ,然後用以下代碼替換它:
override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? { selectedCategories = self.results[UInt(indexPath.row)] as! CategoryModel return indexPath }
上面聲明的方法將會在用戶點擊某個單元格的時候,將用戶點擊的類別存儲在 selectedCategory 屬性當中。
編譯並運行這個應用,然後嘗試定位到某個您感興趣的位置(使用模擬器的位置模擬),然後點擊右上角的 “+” 按鈕創建一個新的標記點。點選地圖上的這個標記點,然後點擊其彈出來的氣泡,接下來會彈出這個標記點的詳細信息。隨後,點擊類別文本框,就可以看到如下圖所示的類別列表了:
類別列表
您可以選擇其中一個類別,不過這個操作僅僅只是將其保存到屬性當中。如果您感興趣,可以前往模擬器的 Documents 目錄下面,使用 Realm Browser 查看我們生成的數據庫,在裡面就可以看到我們寫入的數據了,這是不是很令人激動呢?
使用 Realm Browser
通常情況下,使用 defaultRealm() 方法生成的數據庫文件將會存放在 /Users/(Your Account)/Library/Developer/CoreSimulator/Devices/(Simulator ID)/data/Containers/Data/Application/(Application ID)/Documents/ 路徑下面,名為 default.realm 。 Simulator ID 指的是您運行的模擬器的 ID , Application ID 指的是這個應用所分配到的 ID 。
如果您仍然不清楚這個 Realm 數據庫在哪兒的話,那麼使用如下語句,就可以打印處這個數據庫所在的完整位置了:
println(RLMRealm.defaultRealm().path)
在這個 Documents 目錄下面,我們可能會看到兩個文件。一個是 default.realm 文件,這個是數據庫文件,裡面就是數據的存放點了。而另一個則是 default.realm.lock 文件,這個文件也有可能不會存在,它是用來當數據庫文件被使用時,防止其它應用對其進行修改的一個文件。
雙擊這個 default.realm 文件,就可以使用 Realm Browser 打開了:
Realm Browser 打開的 default.realm 文件
注意:如果 default.realm 已經在其它應用中打開了,那麼強行打開它就可能會出現異常。
.lock 文件就可以防止對 default.realm 文件的重復操作,在使用 Realm Browser 打開數據庫文件前,請先確保應用沒有在運行,然後刪除 .lock 文件,才能打開。
一旦數據庫在 Realm Browser 中被打開,您將會看到 CategoryModel 類中擁有 6 個對象,這就意味著這個 “ 表 ” 中已經存放了 6 個記錄了。點擊這個類就可以查看這個類當中擁有的具體對象信息。
增加類別
好了,現在我們就可以來實現 “ 為某個物種添加類別 ” 的功能了。
打開 AddNewEntryController.swift ,然後向類中添加以下屬性:
var selectedCategory: CategoryModel!
我們將會用這個屬性來存儲我們在 CategoriesTableViewController 選中的類別。
接下來,找到 unwindFromCategories(segue:) 方法,然後在方法底部添加以下代碼:
selectedCategory = categoriesController.selectedCategories categoryTextField.text = selectedCategory.name
這個方法會在用戶從 categoriesTableViewController 中選擇了一個類別後被調用。在這裡,我們獲取到了這個選擇的類別,然後將其存儲在本地屬性 selectedCategory 當中,接著,我們將它的值填充到文本框裡面。
現在,我們已經完成了類別的獲取,接下來就是要創建第一個物種了!
仍然還是在 AddNewEntryController.swift 當中,向類中再添加一個屬性:
var species: SpeciesModel!
這個屬性將會存儲一個新的物種數據模型對象。
接下來,導入 Realm 框架,然後向類中添加以下方法:
func addNewSpecies() { let realm = RLMRealm.defaultRealm() // 1 realm.beginWriteTransaction() // 2 let newSpecies = SpeciesModel() // 3 // 4 newSpecies.name = nameTextField.text newSpecies.category = selectedCategory newSpecies.speciesDescription = descriptionTextView.text newSpecies.latitude = selectedAnnotation.coordinate.latitude newSpecies.longitude = selectedAnnotation.coordinate.longitude realm.addObject(newSpecies) // 5 realm.commitWriteTransaction() // 6 self.species = newSpecies }
對應的標號注釋如下:
獲取默認的 Realm 數據庫
開啟一個事務序列,准備寫入數據
創建一個 Species 對象實例
接著,設置這個對象的相關值。這些值來自於用戶界面的文本輸入框。
向 realm 中寫入新的 Species 對象
最後,使用 commitWriteTransaction() 提交寫操作事務
在這裡,我們需要使用 “ 輸入驗證 ” ,來確保用戶的輸入是正確的。在工程中已經有了一個存在的 validateFields() 方法來執行輸入驗證的工作,以確保物種名稱和描述不能為空。我們剛剛增加了設置類別的功能,那麼我們應該也要確保類別選擇不能為空。
在 validateFields() 方法中找到以下代碼:
if nameTextField.text.isEmpty || descriptionTextView.text.isEmpty {
將其變更為:
if nameTextField.text.isEmpty || descriptionTextView.text.isEmpty || selectedCategory == nil {
這個方法經能夠確保所有的文本框都有值,並且用戶也已經選擇了一個類別。
接下來,向類中添加以下方法:
override func shouldPerformSegueWithIdentifier(identifier: String?, sender: AnyObject?) -> Bool { if validateFields() { if species == nil { addNewSpecies() } return true } else { return false } }
在上面的代碼中,我們調用了輸入驗證的方法,如果所有文本框都有值的話,那麼就可以添加一個新的物種。
編譯並運行您的應用,單擊 “+” 按鈕來創建一個新的物種。然後輸入其名稱和描述,選擇一個類別,接著單擊 “ 保存 ” 按鈕來將這個物種添加到數據庫中。
添加新的數據
視圖消失了 —— 等等,怎麼什麼都沒有發生呢?什麼情況?
哦對了,我們已經向 Realm 數據庫提交了一個數據,但是我們還沒有在地圖上做出相應的設置和改變。
檢索數據
既然我們已經向數據庫中添加了一個物種了,那麼現在我們希望它能夠在地圖上顯示出來。
如果您想要檢視這個心數據,那麼打開 Realm Browser 就可以查看數據了。記住要先退出模擬器。
添加的物種信息
我們僅僅只能夠看見孤零零的一條記錄,裡面存儲了記錄的名稱、描述信息、經緯度信息、添加的時間。還有最重要的,就是我們看到了連接到 CategoryModel 的 category 記錄,這就意味著我們已經創建好了物種和類別的 “ 一對多 ” 關系。點擊這個藍色的超鏈接,我們就可以查看 CategoryModel 的相關數據了。
好的,回到正題,我們現在需要在地圖上顯示新添加的數據。
打開 SpeciesAnnotation.swift ,然後向類中添加一個新的屬性:
var species: SpeciesModel?
這個屬性將會為這個標記點保存它所擁有的物種信息。
接下來,用以下代碼替換構造器:
init(coordinate: CLLocationCoordinate2D, title: String, sub: Categories, species: SpeciesModel? = nil) { self.coordinate = coordinate self.title = title self.subtitle = sub.rawValue self.species = species }
我們所做的改變,就是給這個構造器方法添加了一個帶默認值的構造器參數,以便可以對 species 屬性進行賦值。默認值為 nil ,這意味著我們可以忽略這個參數,使用前面三個參數進行初始化也是沒有任何問題的。
打開 MapViewController.swift ,然後向類中添加一個新屬性(同樣地,別忘了導入 Realm ):
var results: RLMResults?
如果我們想要在用屬性來存儲一系列物種,那麼我們需要將這個屬性聲明為 RLMResults 類型。要記住,我們是不能夠初始化 RLMResults 對象的,我們必須要通過查詢操作來獲取它的值。
現在我們需要一些方法來獲取所有的物種數據。仍然還是在 MapViewController.swift 當中,向類中添加如下方法:
func populateMap() { mapView.removeAnnotations(mapView.annotations) // 1 if let results = SpeciesModel.allObjects() { // 2 self.results = results for result in results { let species = result as! SpeciesModel let coordinate = CLLocationCoordinate2DMake(species.latitude, species.longitude) let speciesAnnotation = SpeciesAnnotation(coordinate: coordinate, title: species.name, sub: Categories(rawValue: species.category.name)!, species: species) // 3 mapView.addAnnotation(speciesAnnotation) // 4 } } }
對應的標號注釋如下:
首先,我們先清除了地圖上所有存在的標記點,這樣我們就不用考慮其他的要素
然後,我們從 Realm 數據庫中獲取 Species 的全部數據
我們在此創建了一個自定義的 SpeciesAnnotation
最後,我們往 MKMapView 上添加這個標記點
好的,現在我們可以在某處地方吊用這個方法了。找到 viewDidLoad() 然後將這個方法加入到這個方法底部:
populateMap()
這樣就確保了每當地圖視圖控制器加載的時候,地圖就能夠顯示 Species 標記點。
接著,我們僅需要修改標記點的名稱和類別即可。找到 unwindFromAddNewEntry() ,然後使用下列代碼替換掉該方法:
@IBAction func unwindFromAddNewEntry(segue: UIStoryboardSegue) { let addNewEntryController = segue.sourceViewController as! AddNewEntryController let addedSpecies = addNewEntryController.species let addedSpeciesCoordinate = CLLocationCoordinate2DMake(addedSpecies.latitude, addedSpecies.longitude) if lastAnnotation != nil { mapView.removeAnnotation(lastAnnotation) } else { for annotation in mapView.annotations { let currentAnnotation = annotation as! SpeciesAnnotation if currentAnnotation.coordinate.latitude == addedSpeciesCoordinate.latitude && currentAnnotation.coordinate.longitude == addedSpeciesCoordinate.longitude { mapView.removeAnnotation(currentAnnotation) break } } } let annotation = SpeciesAnnotation(coordinate: addedSpeciesCoordinate, title: addedSpecies.name, sub: Categories(rawValue: addedSpecies.category.name)!, species: addedSpecies) mapView.addAnnotation(annotation) lastAnnotation = nil }
這個方法將會在我們從 AddNewEntryController 返回的時候被調用,然後這時候就會有一個新的物種被添加到地圖上方。當我們添加了一個新的物種到地圖上,那麼就會產生一個標記圖標。然後我們想要根據物種的類別來改變其圖標的樣式,在這個代碼裡面,我們就是簡單的移除了最後添加的這個標記點,然後將其替換為有名稱和類別的標記點。
編譯並運行您的應用,創建一些不同的物種種類來查看現在地圖是什麼樣式的吧!
添加的標記點效果
另外一個視圖
您或許已經注意到在地圖視圖的左上角有一個 “ 編輯 ” 的按鈕。為了更好地管理地圖上的記錄點,我們這個應用設置了一個基於文本的表視圖,用來列出地圖上所有的記錄點,這個視圖我們現在命名為 “ 記錄 ” 視圖。現在,這個表視圖仍然還是空的,現在我們就來向裡面填充數據吧!
打開 LogViewController.swift ,然後將 species 屬性替換成以下形式(同樣地,要導入 Realm ):
var species: RLMResults!
在上面的代碼中,我們用 RLMResults 替換掉了之前的一個空數組占位符,這個操作和我們在 MapViewController 所做的一樣。
接下來,找到 viewDidLoad() 方法,然後在 super.viewDidLoad() 語句下添加以下代碼:
species = SpeciesModel.allObjects().sortedResultsUsingProperty("name", ascending: true)
這行代碼會將數據庫中的所有物種全部輸出到 species 當中,並且按照名字進行排列。
接下來,用以下代碼替換 tableView(_:cellForRowAtIndexPath:) :
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCellWithIdentifier("LogCell") as! LogCell var speciesModel: SpeciesModel! speciesModel = species[UInt(indexPath.row)] as! SpeciesModel cell.titleLabel.text = speciesModel.name cell.subtitleLabel.text = speciesModel.category.name cell.iconImageView.image = getImageOfSpecies(speciesModel.category.name) return cell }
這個方法將會展示物種的名字和物種的類別,以及其圖標。
編譯並運行應用,單擊左上角的 “ 編輯 ” 按鈕,然後您就會在表視圖中看到我們之前錄入的物種信息,如圖所示:
記錄界面
刪除記錄
現在我們已經學習了如何在 Realm 中創建記錄數據,但是如果我們不小心添加了錯誤的標記點,或者想要移除之前添加過的物種數據,那麼我們應該要怎麼做呢?因此,我們就需要添加從 Realm 中刪除數據的功能。您會發現這是一個非常簡單的操作。
打開 LogViewController.swift 文件,然後添加以下方法:
func deleteRowAtIndexPath(indexPath: NSIndexPath) { let realm = RLMRealm.defaultRealm() // 1 let objectToDelete = species[UInt(indexPath.row)] as! SpeciesModel // 2 realm.beginWriteTransaction() // 3 realm.deleteObject(objectToDelete) // 4 realm.commitWriteTransaction() // 5 species = SpeciesModel.allObjects().sortedResultsUsingProperty("name", ascending: true) // 6 tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) // 7 }
對應的標號注釋如下:
首先,我們獲取到默認的 Realm 數據庫
然後,我們從數據中找到我們想要刪除的對象
啟動寫操作事務
調用 deleteObject() 方法,將要刪除的對象傳遞進去, realm 會自動幫我們執行刪除操作
接著提交寫操作事務,將刪除操作提交到數據庫當中
一旦我們移除了一個物種,我們需要重新讀取數據
最後,我們更新 UITableViewCell ,將單元格移除
接著,找到 tableView(_:commitEditingStyle: forRowAtIndexPath:) 方法,然後將以下代碼加入到 if 語句塊當中:
deleteRowAtIndexPath(indexPath)
當表視圖執行一個單例刪除操作時,會調用這個協議代理,我們所需要做的就是調用我們剛剛創建的那個方法。
編譯並運行您的應用,查看 “ 記錄 ” 界面,然後在某個記錄上面左滑刪除。隨後關閉模擬器,用 Realm Browser 打開數據庫,我們就可以看到我們成功執行了更改:
執行刪除操作
斷言匹配
我們仍然還想要給這個應用提供一些碉堡的功能,那麼快速查找怎麼樣?在海量的數據中進行查找還是很麻煩的一件事情,但是有了快速查找,一切就都簡單了。我們現在所擁有的這個項目已經包含了一個 UISearchController 控件,您所需要做的就是添加一點小小的修改,讓這個功能能夠在 Realm 中正常工作。
打開 LogViewController.swift ,然後將 searchResults 屬性替換為以下代碼:
var searchResults: RLMResults!
因為我們仍然是執行 “ 檢索 ” 操作,因此我們的數據是存放在 RLMResults 當中的。
向類中添加以下方法:
func filterResultsWithSearchString(searchString: String) { let predicate = "name BEGINSWITH [c]'\(searchString)'" // 1 let scopeIndex = searchController.searchBar.selectedScopeButtonIndex searchResults = SpeciesModel.objectsWhere(predicate) // 2 switch scopeIndex { case 0: searchResults = searchResults.sortedResultsUsingProperty("name", ascending: true) // 3 case 1: searchResults = searchResults.sortedResultsUsingProperty("distance", ascending: true) // 4 case 2: searchResults = searchResults.sortedResultsUsingProperty("created", ascending: true) 5 default: return } }
對應的標號注釋如下:
首先我們創建了一個字符串版本的 “ 斷言 (predicate)” ,在這裡,我們搜索以 searchString 開頭的 name 屬性。 [c] 可以讓 BEGINSWITH 以不區分大小寫的靈敏度來進行查找,要注意, searchString 是被單引號括起來的。
我們根據這個斷言,使用 objectsWhere 這個方法來執行斷言檢索操作。
如果選中的標簽是 “ 名字 ” ,那麼結果就按照 “ 名字 A-Z” 排列
如果選中的標簽是 “ 距離 ” ,那麼就按照距離排列結果
如果選中的標簽是 “ 創建時間 ” ,那麼就按照時間來進行排列。
因為搜索會導致表視圖調用同樣的數據源方法,因此我們需要對 tableView(_:cellForRowAtIndexPath:) 進行小小的修改,以便讓其能夠處理主要的表視圖記錄以及查詢結果。在這個方法裡面,找到以下代碼:
speciesModel = species[UInt(indexPath.row)] as! SpeciesModel
將其替換為以下代碼:
if searchController.active { speciesModel = searchResults[UInt(indexPath.row)] as! SpeciesModel }else { speciesModel = species[UInt(indexPath.row)] as! SpeciesModel }
上面這行代碼將會檢查 searchController 是否激活。如果激活的話,那麼就接收並顯示搜索結果的數據;如果不是的話,那麼就接收並顯示 species 全部數據。
最後,我們需要一個功能,那就是單擊范圍欄上的按鈕時,更變返回結果的排列順序。
將空 scopeChanged 方法用以下代碼來替換:
@IBAction func scopeChanged(sender: UISegmentedControl) { switch sender.selectedSegmentIndex { case 0: species = SpeciesModel.allObjects().sortedResultsUsingProperty("name", ascending: true) case 1: break case 2: species = SpeciesModel.allObjects().sortedResultsUsingProperty("created", ascending: true) default: species = SpeciesModel.allObjects().sortedResultsUsingProperty("name", ascending: true) } tableView.reloadData() }
在上面的代碼中,我們將會檢查范圍欄上的按鈕是哪一個被按下( A-Z ,距離,以及添加日期),然後調用 sortedResultsUsingProperty 來進行排序。通常情況下,這個列表將按照名字來排序。
您可能會注意到,現在按照距離排序這一塊中目前還是空的( case 1 ),那是因為目前數據模型中還不存在 “ 距離 ” 這麼一個玩意兒,因此我們暫時還不需要做這個工作,等到以後添加了再來完善。不過現在,我們已經可以看到它的大致功能了!
編譯並運行您的應用,嘗試一些搜索操作,然後查看結果!
查看不同的結果
提示:在作者的原教程中,搜索功能實際上是無法實現的。如果您在 “ 合適 ” 的地方添加了相關方法,那麼實際上程序仍然還是無法執行搜索功能的。它會提示 cell 的 titleLabel 的值為 nil 。因為在原教程中, Cell 是在 Storyboard 裡面自定義的,而搜索欄則是要顯示一個新的表視圖。如果需要重用自定義的 Cell ,那麼最好需要在 Xib 文件中進行制作。因為如果沒有 init(style:reuseIdentifier:) 方法的 Cell 自定義類,是無法進行重用的。
更新記錄
我們現在已經實現了添加和刪除記錄的功能了,剩下就是更新數據功能了。
如果您試著單擊 LogViewController 中的一個單元格,那麼就會跳轉到 AddNewEntryViewController 頁面,但是這些區域都是空白的。當然,我們首先要做的就是讓這個頁面顯示數據庫中存放的數據,以便讓用戶編輯。
打開 AddNewEntryViewController.swift 文件,然後向類中添加以下方法:
func fillTextFields() { nameTextField.text = species.name categoryTextField.text = species.category.name descriptionTextView.text = species.speciesDescription selectedCategory = species.category }
這個方法將會使用 species 中的數據來填充用戶界面的文本框。記住, AddNewEntryViewController 只有在添加新物種時才會保持文本框為空的狀態。
接下來,向 viewDidLoad() 方法的末尾添加以下語句:
if species == nil { title = " 添加新的物種 " }else { title = " 編輯 \(species.name)" fillTextFields() }
上面這些代碼段設置了導航欄的標題,以通知用戶當前其是在添加新的物種還是在更新一個已存在的物種信息。如果 species 不為空,那麼就調用 fillTextFields 方法來填充文本框。
現在我們需要一個更新功能,以便響應用戶的更改操作。向類中添加以下方法:
func updateSpecies() { let realm = RLMRealm.defaultRealm() realm.beginWriteTransaction() species.name = nameTextField.text species.category = selectedCategory species.speciesDescription = descriptionTextView.text realm.commitWriteTransaction() }
通常情況下,這種方法一般都先獲得默認的 Realm 數據庫,然後將數據寫入的操作放在 beginWriteTransaction() 和 commitWriteTransaction() 方法之間。在這個事務中,我們只是簡單的更新了這三個數據域的值。
這六行短短的代碼就足以完成 Species 記錄的更新操作了哦 ~O(∩_∩)O~
現在我們只需要在用戶單擊保存按鈕的時候調用上述代碼即可。找到 shouldPerformSegueWithIdentifier(_:sender:) ,然後在 return true 語句之前,第一個 if 代碼塊之內添加以下代碼:
else { updateSpecies() }
當恰當的時候,就會調用這個方法來對數據進行更新。
現在打開 LogViewController.swift ,然後將 prepareForSegue(_:sender:) 用以下代碼替換:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "Edit" { let controller = segue.destinationViewController as! AddNewEntryController var selectedSpecies: SpeciesModel! let indexPath = tableView.indexPathForSelectedRow() if searchController.active { let searchResultsController = searchController.searchResultsController as! UITableViewController let indexPathSearch = searchResultsController.tableView.indexPathForSelectedRow() selectedSpecies = searchResults[UInt(indexPathSearch!.row)] as! SpeciesModel }else{ selectedSpecies = species[UInt(indexPath!.row)] as! SpeciesModel } controller.species = selectedSpecies } }
我們在這裡將選中的物種信息傳遞給了 AddNewEntryController 。上面的 if/else 代碼是因為要根據用戶是否是在查看搜索結果來決定的。
編譯並運行您的應用,打開記錄視圖,並且選中一個存在的物種。您應該可以看到文本框中已經填充了數據。
更新數據
把剩下的東西結束掉
要讓我們的應用變得更加完美,我們就需要實現剩下 的功能。
還記不記得我們沒有變法根據距離來排序記錄?我們需要在裡面添加不少的代碼,才能夠正常的運行這個功能,但是這個結果是非常值得的。
打開 Species.swift 文件,然後向類中添加一個新的屬性。
dynamic var distance: Double = 0
這個屬性為保存用戶位置和該記錄點的距離信息。然而,沒有必要去存儲 distance 信息,因為用戶位置會隨時發生改變。我們想讓距離成為這個模型的一部分,但是我們並不想 Realm 來存儲這個數據。
Realm 支持一種被稱為忽視屬性 (ignored properties) 的東西,然後向類中添加以下代碼:
func ignoredProperties() -> NSArray { let propertiesToIgnore = [distance] return propertiesToIgnore }
要實現忽視屬性,只需要聲明一個命名為 ignoredProperties() 的方法,然後返回一個屬性數組,裡面保存有您不想進行存儲的屬性。
由於我們並不會存儲距離這個屬性,很明顯地我們需要自己計算距離。
打開 MapViewController.swift ,添加以下方法:
func updateLocationDistance() { let realm = RLMRealm.defaultRealm() if results != nil { for result in results! { let currentSpecies = result as! SpeciesModel let currentLocation = CLLocation(latitude: currentSpecies.latitude, longitude: currentSpecies.longitude) let distance = currentLocation.distanceFromLocation(mapView.userLocation.location) realm.beginWriteTransaction() currentSpecies.distance = Double(distance) realm.commitWriteTransaction() } } }
對於每個物種,我們計算了這個標記點與用戶當前位置之間的距離。即時我們沒有存儲這個距離信息,我們仍然需要將其存儲在記錄當中,然後將其在寫操作事務中保存這個變化消息。
接下來,在 prepareForSegue(_:sender:) 方法底部添加以下代碼:
else if segue.identifier == "Log" { updateLocationDistance() }
現在,在用戶打開 “ 記錄界面 ” 之前,我們需要調用這個方法來計算距離。
接下來,打開 LogViewController.swift ,然後找到 tableView(_:cellForRowAtIndexPath:) 方法。然後在這個方法底部附近, return 語句之前添加以下代碼:
if speciesModel.distance < 0 { cell.distanceLabel.text = "N/A" }else { cell.distanceLabel.text = String(format: "%.2fkm", speciesModel.distance / 1000) }
最後,找到 scopeChanged() 然後將 case 1 中的 break 替換成以下代碼:
species = SpeciesModel.allObjects().sortedResultsUsingProperty("distance", ascending: true)
編譯並運行應用,然後 …… 呃?怎麼崩潰掉了?
'RLMException`, reason: 'Column count does not match interface - migration required'
什麼鬼?
當我們向 Species 模型中添加了一個新的 distance 屬性的時候,我們就對架構( schema ) 進行了變更,但是我們並沒有告訴 Realm 如何處理這個新增的數據段。從舊版本的數據庫遷移( migrate ) 到新版本的數據庫的操作超出了本教程的范圍。這並不是 Realm 獨有的問題, Core Data 同樣也需要在添加、變更或者刪除新的數據段的時候進行遷移操作。
本教程的簡單解決方案就是將模擬器的應用移除掉即可,然後重新編譯並運行應用程序。這將會讓應用創建一個全新的數據庫,使用新的架構。
從模擬器刪除這個應用,接下來編譯和運行這個應用。然後添加新的物種,接著打開這個記錄視圖,這時候我們就可以看到如下所示的距離信息:
距離信息
您或許需要模擬一個位置以便能夠計算當前距離,在模擬器菜單欄上,選擇 Debug\Location ,然後選擇列表中的一個位置模擬。
接下來該何去何從?
您可以點擊此處下載完整的項目
在本教程中,我們學習了如何創建、更新、刪除以及查找 Realm 數據庫中的數據記錄,以及如何使用斷言來進行查找,還有按屬性名對結果進行排序的方法。
您可能要問了: “ 看起來 Realm 似乎是一個新項目,我感覺在一個完備的應用中使用它可能並不穩定 ” 。
Realm 最近才想公眾開放,但是早在 2012 年它就已經在公司級別的產品中使用了。我個人已經在我的既有項目中使用 Realm 了,而且似乎運轉起來相當不錯。
如果您使用 Objective-C ,那麼 Realm 是十分穩定的。對於 Swift 來說,由於 Swift 版本並不穩定,因此在使用 Realm 可能會遭遇到版本更迭所引發的一系列語法問題。不過隨著 Swift 的更新,相信 Swift 的版本改動會越來越少, Realm 在 Swift 上也會越來越穩定。
對於 Realm 來說,它還有許多在本教程沒有介紹到的特點:
遷移( Migrations ) :在本教程中,我們看到了對 Realm 架構的修改導致了錯誤的產生。要學習關於如何在多版本之間遷移數據庫的只是,請查看 Realm 說明文檔的 “ 遷移( migrations) ” 部分。
其他類型的 Realm 數據庫 :在本教程中,我們一直都是使用著 “ 默認 ”Realm 數據庫,但是我們仍然還可以使用其他類型的 Realm 數據庫,比如說不存儲數據的 “ 內存數據庫( in-memory realm ) ” 。我們也可以使用多個 Realm 數據庫,如果我們享用多個數據庫來保存多種類型的數據的話。
關於 Realm 的更多信息,您可以查看 官方文檔 ,我發現這個文檔真的寫得十分詳盡。
如果您對本教程有什麼建議和意見,請到評論區進行討論,我會盡快處理這些建議和意見的 ~
原文 Realm Tutorial
原文作者 Bill Kastanakis
譯者及改編 星夜暮晨(QQ:412027805)