數(shù)據(jù)持久化方案解析(六) —— 基于Realm的持久化存儲(chǔ)(二)

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.12.21 星期五

前言

數(shù)據(jù)的持久化存儲(chǔ)是移動(dòng)端不可避免的一個(gè)問(wèn)題,很多時(shí)候的業(yè)務(wù)邏輯都需要我們進(jìn)行本地化存儲(chǔ)解決和完成,我們可以采用很多持久化存儲(chǔ)方案,比如說(shuō)plist文件(屬性列表)、preference(偏好設(shè)置)、NSKeyedArchiver(歸檔)、SQLite 3CoreData,這里基本上我們都用過(guò)。這幾種方案各有優(yōu)缺點(diǎn),其中,CoreData是蘋(píng)果極力推薦我們使用的一種方式,我已經(jīng)將它分離出去一個(gè)專題進(jìn)行說(shuō)明講解。這個(gè)專題主要就是針對(duì)另外幾種數(shù)據(jù)持久化存儲(chǔ)方案而設(shè)立。
1. 數(shù)據(jù)持久化方案解析(一) —— 一個(gè)簡(jiǎn)單的基于SQLite持久化方案示例(一)
2. 數(shù)據(jù)持久化方案解析(二) —— 一個(gè)簡(jiǎn)單的基于SQLite持久化方案示例(二)
3. 數(shù)據(jù)持久化方案解析(三) —— 基于NSCoding的持久化存儲(chǔ)(一)
4. 數(shù)據(jù)持久化方案解析(四) —— 基于NSCoding的持久化存儲(chǔ)(二)
5. 數(shù)據(jù)持久化方案解析(五) —— 基于Realm的持久化存儲(chǔ)(一)

開(kāi)始

首先看下寫(xiě)作環(huán)境

Swift 4.2, iOS 12, Xcode 10

Realm是一個(gè)跨平臺(tái)的移動(dòng)數(shù)據(jù)庫(kù)解決方案,專為移動(dòng)應(yīng)用程序而設(shè)計(jì),可以與iOS項(xiàng)目集成。 與Core Data的封裝不同,Realm不依賴于Core Data甚至是SQLite后端。

本教程將向您介紹iOS上Realm的基本功能。 在本教程結(jié)束時(shí),您將了解如何鏈接Realm框架,創(chuàng)建模型,執(zhí)行查詢和更新記錄。

這是一個(gè)場(chǎng)景:你已經(jīng)接受了國(guó)家公園管理局的實(shí)習(xí)生職位。 您的工作是記錄在美國(guó)最大的國(guó)家公園中發(fā)現(xiàn)的物種。

您需要助理來(lái)記錄您的發(fā)現(xiàn),但由于該機(jī)構(gòu)沒(méi)有預(yù)算來(lái)雇用新的,您決定為自己創(chuàng)建一個(gè)虛擬助手:一個(gè)名為Agents Partner的應(yīng)用程序。

在Xcode中打開(kāi)啟動(dòng)項(xiàng)目。 目前,該應(yīng)用程序僅包含使用MapKit的地圖功能,該功能已在項(xiàng)目中設(shè)置。

啟動(dòng)項(xiàng)目缺少Realm,所以是時(shí)候添加它了。

注意:本教程是針對(duì)Realm 3.11.1編寫(xiě)的。

安裝Realm的有效方法是使用CocoaPods。

在starter項(xiàng)目的根目錄中,創(chuàng)建一個(gè)名為Podfile的新文件。 復(fù)制以下文本并將其粘貼到新創(chuàng)建的文件中:

platform :ios, '12.0'
use_frameworks!

target 'Agents Partner' do
  pod 'RealmSwift'
end

保存并關(guān)閉文件。

在終端和項(xiàng)目的根目錄中,運(yùn)行以下命令:

pod install

這告訴CocoaPods掃描你的Podfile并安裝你在文件中列出的任何pod。 很簡(jiǎn)約!

Realm可能需要安裝一會(huì),所以請(qǐng)密切關(guān)注您的終端。 一旦完成,您將看到底部附近的一條線,Pod installation complete!!

在Finder中,打開(kāi)starter項(xiàng)目的根目錄。 請(qǐng)注意CocoaPods添加的文件夾以及Agents Partner.xcworkspace。

如果您在Xcode中打開(kāi)了起始項(xiàng)目,請(qǐng)立即關(guān)閉它并通過(guò)雙擊該文件打開(kāi).xcworkspace。 當(dāng)您想要處理項(xiàng)目時(shí),需要打開(kāi)此文件。

您只需使用CocoaPods設(shè)置Realm。 構(gòu)建并運(yùn)行項(xiàng)目以確保編譯所有內(nèi)容。 如果一切按預(yù)期進(jìn)行,你會(huì)看到:


Concepts and Classes Overview - 概念和類概覽

為了更好地理解Realm的作用,下面是一些關(guān)于您將在本教程中使用的類的概念和信息:

  • Realm:Realm實(shí)例是框架的核心。它是您的基礎(chǔ)數(shù)據(jù)庫(kù)的訪問(wèn)點(diǎn),如Core Data managed object context。您可以使用Realm()初始值設(shè)定項(xiàng)創(chuàng)建實(shí)例。
  • Object:這是您的Realm模型。創(chuàng)建模型的行為定義了數(shù)據(jù)庫(kù)的schema。要?jiǎng)?chuàng)建模型,請(qǐng)將Object子類化并定義要作為屬性存儲(chǔ)的字段。
  • Relationships:通過(guò)聲明要引用的Object類型的屬性,可以在對(duì)象之間創(chuàng)建一對(duì)多關(guān)系。您可以通過(guò)List類型的屬性創(chuàng)建多對(duì)一和多對(duì)多關(guān)系。
  • Write Transactions:數(shù)據(jù)庫(kù)中的任何操作,如創(chuàng)建,編輯或刪除對(duì)象,都必須通過(guò)在Realm實(shí)例上調(diào)用write(_ :)來(lái)在寫(xiě)入中執(zhí)行。
  • Queries:要從數(shù)據(jù)庫(kù)中檢索對(duì)象,請(qǐng)使用查詢。最簡(jiǎn)單的查詢形式是在Realm實(shí)例上調(diào)用objects()并傳入您正在尋找的Object的類。如果您的數(shù)據(jù)檢索需求更加復(fù)雜,您可以使用謂詞,鏈接查詢并對(duì)結(jié)果進(jìn)行排序。
  • ResultsResults是您從對(duì)象查詢返回的自動(dòng)更新容器類型。它們與常規(guī)數(shù)組Arrays有許多相似之處,包括下標(biāo)語(yǔ)法。

通過(guò)對(duì)Realm的簡(jiǎn)要介紹,是時(shí)候讓你構(gòu)建項(xiàng)目的其余部分。


Your First Model

Models組打開(kāi)Specimen.swift并添加以下實(shí)現(xiàn):

import Foundation
import RealmSwift

class Specimen: Object {
  @objc dynamic var name = ""
  @objc dynamic var specimenDescription = ""
  @objc dynamic var latitude = 0.0
  @objc dynamic var longitude = 0.0
  @objc dynamic var created = Date()
}

上面的代碼添加了一些屬性:

namesamplesDescription存儲(chǔ)樣本的名稱和描述。 Realm中的特定數(shù)據(jù)類型(如字符串)必須使用值初始化。 在這種情況下,您使用空字符串初始化它們。

latitudelongitude存儲(chǔ)樣本的坐標(biāo)。 在這里,您將類型設(shè)置為Double并使用0.0初始化它們。

created存儲(chǔ)樣本的創(chuàng)建日期。 Date()返回當(dāng)前日期,以便使用該值初始化該屬性。

在Realm中創(chuàng)建第一個(gè)模型后,您是否準(zhǔn)備好在一個(gè)小挑戰(zhàn)中使用這些知識(shí)?

標(biāo)本應(yīng)分為不同的類別。 挑戰(zhàn)在于自己創(chuàng)建一個(gè)Category模型。 將文件命名為Category.swift,并為新模型提供名為name的單個(gè)String屬性。

如果您想檢查您的工作,解決方案如下:

Category.swift看起來(lái)類似如下

import Foundation
import RealmSwift

class Category: Object {
  @objc dynamic var name = "" 
}

您有一個(gè)Category模型,您需要以某種方式與Specimen模型相關(guān)聯(lián)。

回想一下上面的說(shuō)明,聲明您可以通過(guò)聲明具有要鏈接的適當(dāng)模型的屬性來(lái)創(chuàng)建模型之間的關(guān)系。

打開(kāi)Specimen.swift并在其他屬性下面添加以下聲明:

@objc dynamic var category: Category!

這在SpecimenCategory之間建立了一對(duì)多的關(guān)系。 這意味著每個(gè)Specimen只能屬于一個(gè)Category,但每個(gè)Category可以有許多Specimens。

您現(xiàn)在已經(jīng)擁有了基本數(shù)據(jù)模型。 是時(shí)候?qū)⒁恍┯涗浱砑拥侥臄?shù)據(jù)庫(kù)了!


Adding Records - 添加記錄

當(dāng)用戶添加新樣本時(shí),他們可以輸入樣本名稱并選擇一個(gè)類別。 打開(kāi)CategoriesTableViewController.swift。 此視圖控制器在table view中顯示類別列表,以便用戶可以選擇一個(gè)。

在開(kāi)始編寫(xiě)代碼以集成Realm之前,您需要導(dǎo)入RealmSwift框架。 將以下行添加到文件的頂部,在import UIKit下面:

import RealmSwift

您將使用一些默認(rèn)categories填充此table view。 這些Category實(shí)例可以存儲(chǔ)在Results的實(shí)例中。

CategoriesTableViewController現(xiàn)在有一個(gè)categories數(shù)組作為占位符。 在類定義的頂部找到以下代碼:

var categories: [Any] = []

用以下內(nèi)容替換該代碼:

let realm = try! Realm()
lazy var categories: Results<Category> = { self.realm.objects(Category.self) }()

如果要獲取對(duì)象,則始終可以定義所需的模型。 在上面的代碼中,首先創(chuàng)建一個(gè)Realm實(shí)例,然后通過(guò)調(diào)用objects(_:)來(lái)填充categories,并傳入所需模型類型的類名。

注意:為了簡(jiǎn)化本教程中所需的代碼,您正在使用try!調(diào)用拋出錯(cuò)誤的Realm方法時(shí)。 在您自己的代碼中,您應(yīng)該使用trydo / catch來(lái)捕獲和處理錯(cuò)誤。

您希望為用戶提供一些默認(rèn)類別,以便在應(yīng)用首次運(yùn)行時(shí)進(jìn)行選擇。

將以下輔助方法添加到類定義中:

private func populateDefaultCategories() {
  if categories.count == 0 { // 1
    try! realm.write() { // 2
      let defaultCategories =
        ["Birds", "Mammals", "Flora", "Reptiles", "Arachnids" ] // 3
      
      for category in defaultCategories { // 4
        let newCategory = Category()
        newCategory.name = category
        
        realm.add(newCategory)
      }
    }
    
    categories = realm.objects(Category.self) // 5
  }
}

以下是每個(gè)編號(hào)行的內(nèi)容:

  • 1) 如果count等于0,則表示數(shù)據(jù)庫(kù)沒(méi)有Category記錄。 這是第一次運(yùn)行應(yīng)用程序時(shí)的情況。
  • 2) 這將在realm上啟動(dòng)事務(wù),您現(xiàn)在可以將一些記錄添加到數(shù)據(jù)庫(kù)中。
  • 3) 在這里,您創(chuàng)建默認(rèn)類別名稱列表,然后迭代它們。
  • 4) 對(duì)于每個(gè)類別名稱,您可以創(chuàng)建一個(gè)新的Category實(shí)例,填充name并將該對(duì)象添加到realm
  • 5) 您獲取您創(chuàng)建的所有類別并將其存儲(chǔ)在categories中。

將以下行添加到viewDidLoad()的末尾:

populateDefaultCategories()

這會(huì)調(diào)用輔助方法在視圖加載時(shí)填充測(cè)試類別。

現(xiàn)在您有了一些數(shù)據(jù),您將更新table view數(shù)據(jù)源方法以顯示類別。 查找tableView(_:cellForRowAt :)并在return cell之前添加以下內(nèi)容:

let category = categories[indexPath.row]
cell.textLabel?.text = category.name

此實(shí)現(xiàn)基于index pathcategories中檢索類別。 然后,它設(shè)置單元格的文本標(biāo)簽以顯示類別的name。

接下來(lái),在您添加到CategoriesTableViewController的其他屬性下面添加此屬性:

var selectedCategory: Category!

您將使用此屬性存儲(chǔ)當(dāng)前選定的Category。

找到tableView(_:willSelectRowAtIndexPath :)并在return indexPath之前添加以下內(nèi)容:

selectedCategory = categories[indexPath.row]

這會(huì)將用戶的選擇存儲(chǔ)在您在上面聲明的屬性selectedCategory中。

構(gòu)建并運(yùn)行您的應(yīng)用程序。

將地圖縮放并平移到有趣的地方,并通過(guò)點(diǎn)擊右上角的+按鈕創(chuàng)建新的注釋。 點(diǎn)擊地圖圖釘將其選中,然后點(diǎn)擊注釋數(shù)據(jù)以編輯詳細(xì)信息。 最后,點(diǎn)擊Categorytext field以查看類別列表,如下所示:

您可以選擇一個(gè)類別,但只將其保存到屬性中,而不是保存在數(shù)據(jù)庫(kù)中的任何其他位置。 很高興看到應(yīng)用程序中顯示了類別,但是在數(shù)據(jù)庫(kù)中查看記錄總是令人放心。 您可以通過(guò)Realm Browser執(zhí)行此操作。


Introducing the Realm Browser

Realm包含用于讀取和編輯數(shù)據(jù)庫(kù)的Realm Browser。 Realm數(shù)據(jù)庫(kù)格式是專有的,不是人類可讀的。

您可以在此處here下載Realm Browser。


Working With Realm Browser

在開(kāi)發(fā)應(yīng)用程序時(shí)了解Realm數(shù)據(jù)庫(kù)的存儲(chǔ)位置非常重要 - 您可以使用一個(gè)巧妙的技巧來(lái)找出它的位置。

打開(kāi)MapViewController.swift并將以下行添加到現(xiàn)有import語(yǔ)句下面的文件頂部:

import RealmSwift

在調(diào)用super.viewDidLoad()之后,將以下行添加到viewDidLoad()

print(Realm.Configuration.defaultConfiguration.fileURL!)

此行將數(shù)據(jù)庫(kù)位置打印到調(diào)試控制臺(tái)。 這是以后使用Realm Browser瀏覽數(shù)據(jù)庫(kù)的一步。

構(gòu)建并運(yùn)行您的應(yīng)用程序,您將看到它在Xcode控制臺(tái)中報(bào)告數(shù)據(jù)庫(kù)的位置。

轉(zhuǎn)到數(shù)據(jù)庫(kù)位置的最簡(jiǎn)單方法是打開(kāi)Finder,按Shift-Command-G并粘貼應(yīng)用報(bào)告的路徑。

在Finder中打開(kāi)文件夾后,您可能會(huì)看到一個(gè)或兩個(gè)文件。 其中一個(gè)是default.realm,它是您的數(shù)據(jù)庫(kù)文件。 第二個(gè)文件可能存在也可能不存在,是default.realm.lock。 鎖定文件可防止在使用數(shù)據(jù)庫(kù)時(shí)修改其他應(yīng)用程序。

如果您尚未下載Realm Browser,請(qǐng)從App Store下載。 雙擊default.realm以使用Realm Browser打開(kāi)它:

Realm Browser中打開(kāi)數(shù)據(jù)庫(kù)后,您會(huì)看到Category旁邊有一個(gè)5。 這意味著該類包含五個(gè)記錄。 單擊一個(gè)類以檢查其中包含的各個(gè)字段。


Adding Categories

您現(xiàn)在可以設(shè)置邏輯來(lái)設(shè)置Specimencategory。

打開(kāi)AddNewEntryController.swift并在現(xiàn)有import語(yǔ)句下面導(dǎo)入RealmSwift framework

import RealmSwift

將以下屬性添加到類中:

var selectedCategory: Category!

您將使用它來(lái)存儲(chǔ)選定的Category

接下來(lái),找到unwindFromCategories(segue :)并在其中添加以下實(shí)現(xiàn):

if segue.identifier == "CategorySelectedSegue" {
  let categoriesController = segue.source as! CategoriesTableViewController
  selectedCategory = categoriesController.selectedCategory
  categoryTextField.text = selectedCategory.name
}

當(dāng)用戶從您在上一步中設(shè)置的CategoriesTableViewController中選擇一個(gè)類別時(shí),將調(diào)用unwindFromCategories(segue :)。 在這里,您檢索所選類別,將其存儲(chǔ)在selectedCategory中,并使用類別名稱填寫(xiě)text field。

您可以繼續(xù)創(chuàng)建您的第一個(gè)Specimen!


Adding Specimens

仍然在AddNewEntryController.swift中,再向該類添加一個(gè)屬性:

var specimen: Specimen!

此屬性存儲(chǔ)新的specimen對(duì)象。

接下來(lái),將此輔助方法添加到類中:

func addNewSpecimen() {
  let realm = try! Realm() // 1
    
  try! realm.write { // 2
    let newSpecimen = Specimen() // 3
      
    newSpecimen.name = nameTextField.text! // 4
    newSpecimen.category = selectedCategory
    newSpecimen.specimenDescription = descriptionTextField.text
    newSpecimen.latitude = selectedAnnotation.coordinate.latitude
    newSpecimen.longitude = selectedAnnotation.coordinate.longitude
      
    realm.add(newSpecimen) // 5
    specimen = newSpecimen // 6
  }
}

以下是上面代碼的作用:

  • 1) 首先,像以前一樣獲取Realm實(shí)例。
  • 2) 啟動(dòng)寫(xiě)入事務(wù)以添加新的Specimen
  • 3) 創(chuàng)建一個(gè)新的Specimen實(shí)例。
  • 4) 分配Specimen值。 值來(lái)自用戶界面中的輸入文本字段,選定的類別和地圖注釋中的坐標(biāo)。
  • 5) 將新Specimen添加到領(lǐng)域。
  • 6) 將新Specimen分配給Specimen屬性。

您需要某種驗(yàn)證器來(lái)確保所有字段都填充在您的Specimen中。 存在AddNewEntryViewController中的validateFields()以檢查Specimen名稱和描述。 由于您已添加了為樣本分配類別的功能,因此您也將檢查該字段。

validateFields()中找到如下所示的行:

if nameTextField.text!.isEmpty || descriptionTextField.text!.isEmpty {

修改這一行,如下所示:

if 
  nameTextField.text!.isEmpty || 
  descriptionTextField.text!.isEmpty || 
  selectedCategory == nil {

這將驗(yàn)證是否已填充所有字段以及您是否已選擇類別。

接下來(lái),將以下方法添加到類中:

override func shouldPerformSegue(
  withIdentifier identifier: String, 
  sender: Any?
  ) -> Bool {
    if validateFields() {
      addNewSpecimen()
        
      return true
    } else {
      return false
    }
}

在上面的代碼中,您調(diào)用方法來(lái)驗(yàn)證字段。 如果所有內(nèi)容都已填寫(xiě),則添加新樣本并返回true;否則,你返回false

建立并運(yùn)行。 點(diǎn)擊+按鈕創(chuàng)建一個(gè)新樣本。 填寫(xiě)名稱和說(shuō)明,選擇一個(gè)類別,然后點(diǎn)擊Confirm將您的Specimen添加到數(shù)據(jù)庫(kù)。

視圖控制器dismiss,但似乎沒(méi)有任何事情發(fā)生。 這是怎么回事?

您已將記錄發(fā)布到您的realm,但您尚未使用新樣本填充地圖。


Retrieving Records

您已將樣本添加到要在地圖上顯示的數(shù)據(jù)庫(kù)中。

首先看一下Realm Browser中更新的數(shù)據(jù)庫(kù):

您將看到填充了其字段的單個(gè)樣本,以及MKAnnotation中的緯度和經(jīng)度。 您還可以看到標(biāo)本類別的鏈接;這意味著您的一對(duì)多Category關(guān)系正在按預(yù)期工作。

單擊Specimen記錄中的Category以查看Category記錄本身。

接下來(lái),您將在應(yīng)用程序中填充地圖。

打開(kāi)SpecimenAnnotation.swift并向該類添加屬性:

var specimen: Specimen?

這保存了注釋的Specimen。

接下來(lái),使用以下內(nèi)容替換初始化程序:

init(
  coordinate: CLLocationCoordinate2D, 
  title: String, 
  subtitle: String, 
  specimen: Specimen? = nil
  ) {
    self.coordinate = coordinate
    self.title = title
    self.subtitle = subtitle
    self.specimen = specimen
}

這里的更改是添加一個(gè)傳遞Specimen的選項(xiàng)。 樣本的默認(rèn)值為nil,這意味著如果您愿意,可以省略該參數(shù)。 如果沒(méi)有標(biāo)本,應(yīng)用程序的其余部分仍然可以使用前三個(gè)參數(shù)調(diào)用初始化程序。

打開(kāi)MapViewController.swift并向該類添加一個(gè)新屬性:

var specimens = try! Realm().objects(Specimen.self)

由于您希望在此屬性中存儲(chǔ)標(biāo)本集合,因此請(qǐng)向Realm實(shí)例詢問(wèn)Specimen類型的所有對(duì)象。

現(xiàn)在,將以下方法添加到類中:

func populateMap() {
  mapView.removeAnnotations(mapView.annotations) // 1

  specimens = try! Realm().objects(Specimen.self) // 2

  // Create annotations for each one
  for specimen in specimens { // 3
    let coord = CLLocationCoordinate2D(
      latitude: specimen.latitude, 
      longitude: specimen.longitude);
    let specimenAnnotation = SpecimenAnnotation(
      coordinate: coord,
      title: specimen.name,
      subtitle: specimen.category.name,
      specimen: specimen)
    mapView.addAnnotation(specimenAnnotation) // 4
  }
}

下面進(jìn)行細(xì)分:

  • 1) 清除地圖上的所有現(xiàn)有注釋以重新開(kāi)始。
  • 2) 刷新您的specimens屬性。
  • 3) 遍歷specimens并使用樣本的坐標(biāo)以及其namecategory創(chuàng)建SpecimenAnnotation。
  • 4) 將每個(gè)samplesAnnotation添加到MKMapView。

你需要從某個(gè)地方調(diào)用這個(gè)方法。 找到viewDidLoad()并將此行添加到其實(shí)現(xiàn)的末尾:

populateMap()

這可確保地圖在視圖控制器加載時(shí)顯示樣本。

現(xiàn)在,您將更改注釋以包含樣本名稱和類別。 找到unwindFromAddNewEntry(segue :)并用以下實(shí)現(xiàn)替換該方法:

@IBAction func unwindFromAddNewEntry(segue: UIStoryboardSegue) {
  let addNewEntryController = segue.source as! AddNewEntryViewController
  let addedSpecimen = addNewEntryController.specimen!
  let addedSpecimenCoordinate = CLLocationCoordinate2D(
    latitude: addedSpecimen.latitude,
    longitude: addedSpecimen.longitude)
    
  if let lastAnnotation = lastAnnotation {
    mapView.removeAnnotation(lastAnnotation)
  } else {
    for annotation in mapView.annotations {
      if let currentAnnotation = annotation as? SpecimenAnnotation {
        if currentAnnotation.coordinate.latitude == addedSpecimenCoordinate.latitude &&
          currentAnnotation.coordinate.longitude == addedSpecimenCoordinate.longitude {
            mapView.removeAnnotation(currentAnnotation)
            break
        }
      }
    }
  }
    
  let annotation = SpecimenAnnotation(
    coordinate: addedSpecimenCoordinate,
    title: addedSpecimen.name,
    subtitle: addedSpecimen.category.name,
    specimen: addedSpecimen)
    
  mapView.addAnnotation(annotation)
  lastAnnotation = nil;
}

一旦從AddNewEntryController返回并且有一個(gè)新的樣本要添加到地圖中,系統(tǒng)將調(diào)用此方法。 將新樣本添加到地圖時(shí),會(huì)獲得通用注釋圖標(biāo)。 對(duì)于您的類別,您希望將該圖標(biāo)更改為特定于類別的圖標(biāo)。

在這里,您刪除添加到地圖的最后一個(gè)注釋,并將其替換為顯示樣本名稱和類別的注釋。

建立并運(yùn)行。 創(chuàng)建一些不同類別的新標(biāo)本,并查看地圖如何更新:

1. A Different View

您可能已經(jīng)注意到地圖視圖左上角的Log按鈕。 除了地圖之外,該應(yīng)用程序還有一個(gè)基于文本的table view,列出了所有注釋,稱為Log View。 接下來(lái),您將使用一些數(shù)據(jù)填充此表視圖。

打開(kāi)LogViewController.swift并導(dǎo)入RealmSwift

import RealmSwift

然后,用以下內(nèi)容替換specimens屬性:

var specimens = try! Realm().objects(Specimen.self)
  .sorted(byKeyPath: "name", ascending: true)

在上面的代碼中,您將占位符數(shù)組替換為包含SpecimensResults,就像在MapViewController中一樣。 它們將按name排序。

接下來(lái),在return cell之前將以下內(nèi)容添加到tableView(_:cellForRowAt :)

let specimen = specimens[indexPath.row]

cell.titleLabel.text = specimen.name
cell.subtitleLabel.text = specimen.category.name

switch specimen.category.name {
case "Uncategorized":
  cell.iconImageView.image = UIImage(named: "IconUncategorized")
case "Reptiles":
  cell.iconImageView.image = UIImage(named: "IconReptile")
case "Flora":
  cell.iconImageView.image = UIImage(named: "IconFlora")
case "Birds":
  cell.iconImageView.image = UIImage(named: "IconBird")
case "Arachnid":
  cell.iconImageView.image = UIImage(named: "IconArachnid")
case "Mammals":
  cell.iconImageView.image = UIImage(named: "IconMammal")
default:
  cell.iconImageView.image = UIImage(named: "IconUncategorized")
}

此方法使用樣本的namecategory填充單元格。

構(gòu)建并運(yùn)行您的應(yīng)用程序。 點(diǎn)擊Log,您將在table view中看到所有輸入的樣本,如下所示:

2. Fetches With Predicates

您希望自己的應(yīng)用有一個(gè)方便的搜索功能。 您的starter項(xiàng)目包含一個(gè)UISearchController實(shí)例;您將添加一些特定于您的應(yīng)用的修改,以使其與Realm一起使用。

LogViewController.swift中,將searchResults屬性替換為以下內(nèi)容:

var searchResults = try! Realm().objects(Specimen.self)

向類中添加這個(gè)方法

func filterResultsWithSearchString(searchString: String) {
  let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString) // 1
  let scopeIndex = searchController.searchBar.selectedScopeButtonIndex // 2
  let realm = try! Realm()
    
  switch scopeIndex {
  case 0:
    searchResults = realm.objects(Specimen.self)
      .filter(predicate).sorted(byKeyPath: "name", ascending: true) // 3
  case 1:
    searchResults = realm.objects(Specimen.self).filter(predicate)
      .sorted(byKeyPath: "created", ascending: true) // 4
  default:
    searchResults = realm.objects(Specimen.self).filter(predicate) // 5
  }
}

以下是上述函數(shù)的作用:

  • 1) 首先,創(chuàng)建一個(gè)謂詞,用于搜索以searchString開(kāi)頭的name。 BEGINSWITH之后的[c]表示不區(qū)分大小寫(xiě)的搜索。
  • 2) 然后,從搜索欄中獲取對(duì)當(dāng)前所選范圍索引的引用。
  • 3) 如果選擇了第一個(gè)分段按鈕,則按名稱升序?qū)Y(jié)果進(jìn)行排序。
  • 4) 如果選擇了第二個(gè)按鈕,則按創(chuàng)建日期升序?qū)Y(jié)果進(jìn)行排序。
  • 5) 如果未選擇任何按鈕,請(qǐng)不要對(duì)結(jié)果進(jìn)行排序,按照從數(shù)據(jù)庫(kù)返回的順序進(jìn)行排序。

現(xiàn)在,當(dāng)用戶與search field交互時(shí),您實(shí)際上將執(zhí)行過(guò)濾。 在updateSearchResults(for :)中,在方法的開(kāi)頭添加以下兩行:

let searchString = searchController.searchBar.text!
filterResultsWithSearchString(searchString: searchString)

由于搜索結(jié)果table view調(diào)用相同的數(shù)據(jù)源方法,因此您需要對(duì)tableView(_:cellForRowAt :)進(jìn)行一些小的更改,以處理主日志表視圖和搜索結(jié)果。 在該方法中,找到分配給specimen的那一行:

let specimen = specimens[indexPath.row]

刪除它并用以下內(nèi)容替換它:

let specimen = searchController.isActive ?
  searchResults[indexPath.row] : specimens[indexPath.row]

此代碼檢查searchController是否處于活動(dòng)狀態(tài)。 如果是這樣,它將從searchResults中檢索樣本。 如果沒(méi)有,它會(huì)從specimens中取回樣本。

接下來(lái),當(dāng)用戶點(diǎn)擊范圍欄中的按鈕時(shí),您將添加一個(gè)函數(shù)來(lái)對(duì)返回的結(jié)果進(jìn)行排序。

將以下實(shí)現(xiàn)添加到scopeChanged(sender :)

let scopeBar = sender as! UISegmentedControl
let realm = try! Realm()
  
switch scopeBar.selectedSegmentIndex {
case 1:
  specimens = realm.objects(Specimen.self)
    .sorted(byKeyPath: "created", ascending: true)
default:
  specimens = realm.objects(Specimen.self)
    .sorted(byKeyPath: "name", ascending: true)
}
  
tableView.reloadData()

在這里,您可以檢查按下的范圍按鈕(A-ZDate Added)并進(jìn)行排序。 默認(rèn)情況下,列表將按name排序。

構(gòu)建并運(yùn)行您的應(yīng)用程序。 嘗試一些不同的搜索,看看你得到的結(jié)果。


Updating Records

您已經(jīng)介紹了添加記錄,但是當(dāng)您想要更新記錄時(shí)呢?

如果您點(diǎn)擊LogViewController中的單元格,您將轉(zhuǎn)到AddNewEntryViewController,但字段為空。 讓用戶編輯字段的第一步是顯示現(xiàn)有數(shù)據(jù)。

打開(kāi)AddNewEntryViewController.swift并將以下輔助方法添加到類中:

func fillTextFields() {
  nameTextField.text = specimen.name
  categoryTextField.text = specimen.category.name
  descriptionTextField.text = specimen.specimenDescription

  selectedCategory = specimen.category
}

該方法用樣本數(shù)據(jù)填充用戶界面。 請(qǐng)記住,到目前為止,AddNewEntryViewController僅用于新標(biāo)本,因此這些字段始終為空。

接下來(lái),將以下行添加到viewDidLoad()的末尾:

if let specimen = specimen {
  title = "Edit \(specimen.name)"
      
  fillTextFields()
} else {
  title = "Add New Specimen"
}

此代碼設(shè)置導(dǎo)航標(biāo)題以指示用戶是否正在添加或更新樣本。 如果它是現(xiàn)有樣本,您還可以調(diào)用輔助方法來(lái)填充字段。

您需要一種方法來(lái)更新樣本記錄和用戶的更改。 添加以下方法:

func updateSpecimen() {
  let realm = try! Realm()
    
  try! realm.write {
    specimen.name = nameTextField.text!
    specimen.category = selectedCategory
    specimen.specimenDescription = descriptionTextField.text
  }
}

像往常一樣,該方法從獲取Realm實(shí)例開(kāi)始,然后其余部分封裝在write()事務(wù)中。 在事務(wù)內(nèi)部,您更新數(shù)據(jù)字段。

只需要六行代碼來(lái)更新Specimen記錄!

接下來(lái),當(dāng)用戶點(diǎn)擊Confirm時(shí),您將調(diào)用上述方法。 找到shouldPerformSegue(withIdentifier:sender :)并將addNewSpecimen()的調(diào)用替換為以下內(nèi)容:

if specimen != nil {
  updateSpecimen()
} else {
  addNewSpecimen()
}

這會(huì)調(diào)用您的方法在適當(dāng)時(shí)更新數(shù)據(jù)。

打開(kāi)LogViewController.swift并為prepare(for:sender :)添加以下實(shí)現(xiàn):

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if (segue.identifier == "Edit") {
    let controller = segue.destination as! AddNewEntryViewController
    var selectedSpecimen: Specimen!
    let indexPath = tableView.indexPathForSelectedRow
      
    if searchController.isActive {
      let searchResultsController =
        searchController.searchResultsController as! UITableViewController
      let indexPathSearch = searchResultsController.tableView.indexPathForSelectedRow
        
      selectedSpecimen = searchResults[indexPathSearch!.row]
    } else {
      selectedSpecimen = specimens[indexPath!.row]
    }
      
    controller.specimen = selectedSpecimen
  }
}

您將選定的樣本傳遞給AddNewEntryController實(shí)例。 if / else的復(fù)雜性是因?yàn)槿Q于用戶是否正在查看搜索結(jié)果而使所選樣本不同。

構(gòu)建并運(yùn)行您的應(yīng)用程序。 打開(kāi)日志視圖,然后點(diǎn)擊Specimen。 您將看到填充了所有字段并準(zhǔn)備編輯的詳細(xì)信息。

后記

本篇主要講述了基于Realm的持久化存儲(chǔ),感興趣的給個(gè)贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容