數(shù)據(jù)持久化方案解析(十九) —— 基于批插入和存儲(chǔ)歷史等高效CoreData使用示例(一)

版本記錄

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

前言

數(shù)據(jù)的持久化存儲(chǔ)是移動(dòng)端不可避免的一個(gè)問(wèn)題,很多時(shí)候的業(yè)務(wù)邏輯都需要我們進(jìn)行本地化存儲(chǔ)解決和完成,我們可以采用很多持久化存儲(chǔ)方案,比如說(shuō)plist文件(屬性列表)、preference(偏好設(shè)置)、NSKeyedArchiver(歸檔)、SQLite 3、CoreData,這里基本上我們都用過(guò)。這幾種方案各有優(yōu)缺點(diǎn),其中,CoreData是蘋(píng)果極力推薦我們使用的一種方式,我已經(jīng)將它分離出去一個(gè)專(zhuān)題進(jìn)行說(shuō)明講解。這個(gè)專(zhuān)題主要就是針對(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ǔ)(一)
6. 數(shù)據(jù)持久化方案解析(六) —— 基于Realm的持久化存儲(chǔ)(二)
7. 數(shù)據(jù)持久化方案解析(七) —— 基于Realm的持久化存儲(chǔ)(三)
8. 數(shù)據(jù)持久化方案解析(八) —— UIDocument的數(shù)據(jù)存儲(chǔ)(一)
9. 數(shù)據(jù)持久化方案解析(九) —— UIDocument的數(shù)據(jù)存儲(chǔ)(二)
10. 數(shù)據(jù)持久化方案解析(十) —— UIDocument的數(shù)據(jù)存儲(chǔ)(三)
11. 數(shù)據(jù)持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲(chǔ)示例(一)
12. 數(shù)據(jù)持久化方案解析(十二) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲(chǔ)示例(二)
13. 數(shù)據(jù)持久化方案解析(十三) —— 基于Unit Testing的Core Data測(cè)試(一)
14. 數(shù)據(jù)持久化方案解析(十四) —— 基于Unit Testing的Core Data測(cè)試(二)
15. 數(shù)據(jù)持久化方案解析(十五) —— 基于Realm和SwiftUI的數(shù)據(jù)持久化簡(jiǎn)單示例(一)
16. 數(shù)據(jù)持久化方案解析(十六) —— 基于Realm和SwiftUI的數(shù)據(jù)持久化簡(jiǎn)單示例(二)
17. 數(shù)據(jù)持久化方案解析(十七) —— 基于NSPersistentCloudKitContainer的Core Data和CloudKit的集成示例(一)
18. 數(shù)據(jù)持久化方案解析(十八) —— 基于NSPersistentCloudKitContainer的Core Data和CloudKit的集成示例(二)

開(kāi)始

首先看下主要內(nèi)容:

在本教程中,您將學(xué)習(xí)如何借助批處理插入,持久性歷史記錄和派生屬性的有效Core Data使用來(lái)改進(jìn)iOS應(yīng)用。內(nèi)容來(lái)自翻譯。

下面看下寫(xiě)作環(huán)境:

Swift 5, iOS 14, Xcode 12

接著就是主要內(nèi)容了。

Core Data是已存在很長(zhǎng)時(shí)間的古老的Apple框架之一。自從iOS 10中發(fā)布NSPersistentContainer以來(lái),蘋(píng)果公司就向Core Data表示了極大的熱愛(ài)。最新添加的Core Data進(jìn)一步提升了其競(jìng)爭(zhēng)力?,F(xiàn)在有批量插入請(qǐng)求,持久性歷史記錄和派生屬性,這些絕對(duì)可以使Core Data的使用效率更高。

在本教程中,您將通過(guò)提高數(shù)據(jù)存儲(chǔ)效率來(lái)改進(jìn)應(yīng)用程序。您將學(xué)習(xí)如何:

  • Create a batch insert request
  • Query the persistent store’s transaction history
  • Control how and when the UI updates in response to new data

您可能會(huì)在此過(guò)程中拯救人類(lèi)!

注意:本中級(jí)教程假定您具有使用Xcode編寫(xiě)iOS應(yīng)用程序和編寫(xiě)Swift的經(jīng)驗(yàn)。您應(yīng)該已經(jīng)使用過(guò)Core Data,并對(duì)其概念感到滿意。如果您想學(xué)習(xí)基礎(chǔ)知識(shí),可以先嘗試Core Data with SwiftUI tutorial。

Fireballs!他們無(wú)處不在!有人在注意嗎?Fireballs可能是外星人入侵的最初跡象,也可能是即將來(lái)臨的大決戰(zhàn)的預(yù)兆。有人必須保持警惕。這是你的任務(wù)。您已經(jīng)制作了一個(gè)應(yīng)用程序,可以從NASA Jet Propulsion Laboratory (JPL)下載火球瞄準(zhǔn)點(diǎn),以便將它們分組并報(bào)告可疑的火球活動(dòng)。

打開(kāi)啟動(dòng)項(xiàng)目。 看你到目前為止有什么。


Exploring Fireball Watch

構(gòu)建并運(yùn)行該應(yīng)用程序,以便您可以了解其工作方式。 該應(yīng)用程序從JPL下載最新的火球數(shù)據(jù),為每個(gè)火球瞄準(zhǔn)創(chuàng)建記錄并將其存儲(chǔ)在Core Data stack中。 您還可以創(chuàng)建組并將火球添加到組中以進(jìn)行報(bào)告。

啟動(dòng)時(shí),列表將為空,因此請(qǐng)點(diǎn)擊Fireballs列表右上角的刷新按鈕。 很快,該列表就會(huì)填滿。 您可以再次點(diǎn)擊以查看它沒(méi)有為相同數(shù)據(jù)添加重復(fù)記錄。 如果您在某些火球單元上向左滑動(dòng)并刪除了一些,然后再次點(diǎn)擊刷新,則會(huì)看到下載數(shù)據(jù)后重新創(chuàng)建的那些fireballs。

如果點(diǎn)擊Groups選項(xiàng)卡,則可以添加一個(gè)組。 進(jìn)行一些分組,然后返回Fireballs選項(xiàng)卡,然后在列表中點(diǎn)擊一個(gè)火球。 然后,點(diǎn)擊右上角的in-tray按鈕以選擇一個(gè)或多個(gè)包含該火球的組。 當(dāng)您點(diǎn)擊Groups標(biāo)簽中列出的組列表時(shí),它將向您顯示那個(gè)組中所有火球的地圖。

注意:您可以在此處閱讀有關(guān)JPLfireball API here的信息。


Examining the Core Data Stack

現(xiàn)在,看看應(yīng)用程序的Core Data stack是如何設(shè)置的。

打開(kāi)Persistence.swift。 您會(huì)看到一個(gè)名為PersistenceController的類(lèi)。 此類(lèi)處理您的所有Core Data設(shè)置和數(shù)據(jù)導(dǎo)入。 它使用NSPersistentContainer創(chuàng)建一個(gè)標(biāo)準(zhǔn)的SQLite存儲(chǔ),或者創(chuàng)建一個(gè)用于SwiftUI預(yù)覽的內(nèi)存存儲(chǔ)。

persistent containerviewContext是應(yīng)用程序用于獲取請(qǐng)求(生成列表數(shù)據(jù))的managed object context。 這是典型的設(shè)置。 您的模型中有兩個(gè)實(shí)體(entities)FireballFireballGroup。

PersistenceController具有fetchFireballs(),可下載火球數(shù)據(jù)并調(diào)用私有importFetchedFireballs(_ :)以將所得的FireballData struct數(shù)組導(dǎo)入為Fireballmanaged objects。 它使用持久性容器的performBackgroundTask(_ :)作為后臺(tái)任務(wù)來(lái)執(zhí)行此操作。

importFetchedFireballs(_ :)循環(huán)遍歷FireballData數(shù)組,創(chuàng)建一個(gè)managed object并保存managed object context。 由于永久性容器的viewContextautomaticallyMergesChangesFromParent設(shè)置為true,因此在應(yīng)用程序保存所有對(duì)象時(shí),這可能會(huì)使UI停滯。 這是一個(gè)會(huì)使應(yīng)用感覺(jué)很笨拙的問(wèn)題,是您第一次改進(jìn)的目標(biāo)。


Making a Batch Insert Request

報(bào)告的火球列表只會(huì)越來(lái)越大,如果突然出現(xiàn)火球群怎么辦? 火球群可能表明可能有外星人著陸點(diǎn),預(yù)示著新的入侵嘗試!

您希望初始下載盡可能靈活。 您的應(yīng)用程序需要快速使您掌握最新數(shù)據(jù)。 任何暫停,延遲或掛起都是不可接受的。

批量插入可助您一臂之力! 批處理插入請(qǐng)求是一種特殊的持久性存儲(chǔ)請(qǐng)求,它允許您將大量數(shù)據(jù)直接導(dǎo)入到持久性存儲(chǔ)中。 您需要一個(gè)方法來(lái)為此操作創(chuàng)建批量插入請(qǐng)求。 打開(kāi)Persistence.swift并將以下方法添加到PersistenceController

private func newBatchInsertRequest(with fireballs: [FireballData])
  -> NSBatchInsertRequest {
  // 1
  var index = 0
  let total = fireballs.count

  // 2
  let batchInsert = NSBatchInsertRequest(
    entity: Fireball.entity()) { (managedObject: NSManagedObject) -> Bool in
    // 3
    guard index < total else { return true }

    if let fireball = managedObject as? Fireball {
      // 4
      let data = fireballs[index]
      fireball.dateTimeStamp = data.dateTimeStamp
      fireball.radiatedEnergy = data.radiatedEnergy
      fireball.impactEnergy = data.impactEnergy
      fireball.latitude = data.latitude
      fireball.longitude = data.longitude
      fireball.altitude = data.altitude
      fireball.velocity = data.velocity
    }

    // 5
    index += 1
    return false
  }
  return batchInsert
}

此方法采用FireballData對(duì)象數(shù)組,并創(chuàng)建一個(gè)NSBatchInsertRequest來(lái)插入所有對(duì)象。就是這樣:

  • 1) 您首先創(chuàng)建局部變量以保存當(dāng)前循環(huán)索引和總火球計(jì)數(shù)。
  • 2) 使用NSBatchInsertRequest(entity:managedObjectHandler :)創(chuàng)建批處理插入請(qǐng)求。此方法要求您要執(zhí)行的每個(gè)插入都執(zhí)行一個(gè)NSEntity和一個(gè)閉包 —— 每個(gè)火球一個(gè)。如果是最后一次插入,則閉包必須返回true。
  • 3) 在閉包內(nèi)部,您首先要檢查是否已到達(dá)火球數(shù)組的末尾,如果返回true,則完成請(qǐng)求。
  • 4) 在這里插入新數(shù)據(jù)。使用NSManagedObject實(shí)例調(diào)用該閉包。這是一個(gè)新對(duì)象,并檢查其類(lèi)型為Fireball(始終為,但應(yīng)始終安全),然后設(shè)置對(duì)象的屬性以匹配獲取的Fireball數(shù)據(jù)。
  • 5) 最后,您增加索引并返回false,表示插入請(qǐng)求應(yīng)再次調(diào)用閉包。

注意:在iOS 13中,當(dāng)NSBatchInsertRequest首次發(fā)布時(shí),只有一個(gè)初始化程序采用了表示所有要插入數(shù)據(jù)的字典數(shù)組。在iOS 14中,添加了四個(gè)新變體,每個(gè)變體使用閉包樣式的初始化程序以及managed object或字典。有關(guān)更多信息,請(qǐng)參閱 See the Apple documentation for more information。


Batch Inserting Fireballs

這樣就完成了請(qǐng)求創(chuàng)建。 現(xiàn)在,您如何使用它? 將以下方法添加到PersistenceController

private func batchInsertFireballs(_ fireballs: [FireballData]) {
  // 1
  guard !fireballs.isEmpty else { return }

  // 2
  container.performBackgroundTask { context in
    // 3
    let batchInsert = self.newBatchInsertRequest(with: fireballs)
    do {
      try context.execute(batchInsert)
    } catch {
      // log any errors
    }
  }
}

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

  • 1) 首先,請(qǐng)檢查是否有實(shí)際的工作要做,以確保數(shù)組不為空。
  • 2) 然后要求PersistentContainer使用performBackgroundTask(_ :)執(zhí)行后臺(tái)任務(wù)。
  • 3) 創(chuàng)建批處理插入請(qǐng)求,然后執(zhí)行它,捕獲可能引發(fā)的任何錯(cuò)誤。 批處理請(qǐng)求通過(guò)一次事務(wù)將所有數(shù)據(jù)插入持久性存儲(chǔ)(persistent store)中。 由于您的Core Data model已定義了唯一約束,因此它將僅創(chuàng)建不存在的新記錄,并在需要時(shí)更新現(xiàn)有記錄。

最后一項(xiàng)更改:轉(zhuǎn)到fetchFireballs(),而不是調(diào)用self?.importFetchedFireballs($ 0),將其更改為:

self?.batchInsertFireballs($0)

您也可以注釋或刪除importFetchedFireballs(_ :),因?yàn)椴辉傩枰?/p>

注意:如果您想知道,批處理插入請(qǐng)求不能設(shè)置Core Data entity relationship,但是它們將保持現(xiàn)有關(guān)系不變。 有關(guān)更多信息,請(qǐng)參見(jiàn)使用WWDC2019中的 Making Apps with Core Data。

剩下要做的就是構(gòu)建并運(yùn)行!

但是您可能會(huì)注意到有些問(wèn)題。 如果刪除火球,然后再次點(diǎn)擊刷新按鈕,則列表不會(huì)更新。 那是因?yàn)榕幚聿迦胝?qǐng)求將數(shù)據(jù)插入到持久性存儲(chǔ)(persistent store)中,但是視圖上下文(view context)沒(méi)有更新,因此它不知道任何更改。 您可以通過(guò)重啟應(yīng)用來(lái)確認(rèn)這一點(diǎn),然后您將看到所有新數(shù)據(jù)現(xiàn)在都顯示在列表中。

以前,您是在后臺(tái)隊(duì)列上下文(background queue context)中創(chuàng)建對(duì)象并保存上下文,這會(huì)將更改推送到持久性存儲(chǔ)協(xié)調(diào)器(persistent store coordinator)。保存后臺(tái)上下文后,它已從持久性存儲(chǔ)協(xié)調(diào)器自動(dòng)更新,因?yàn)槟言谝晥D上下文中將automaticallyMergeChangesFromParent設(shè)置為true。

持久性存儲(chǔ)(persistent store)請(qǐng)求的部分效率是它們直接在持久性存儲(chǔ)上運(yùn)行,并且避免將數(shù)據(jù)加載到內(nèi)存中或生成上下文保存通知。因此,在應(yīng)用程序運(yùn)行時(shí),您將需要一種新的策略來(lái)更新視圖上下文。


Enabling Notifications

當(dāng)然,在后臺(tái)更新存儲(chǔ)并非不常見(jiàn)。例如,您可能具有一個(gè)用于擴(kuò)展持久性存儲(chǔ)(persistent store)的應(yīng)用程序擴(kuò)展,或者您的應(yīng)用程序支持iCloud,并且您的應(yīng)用程序的存儲(chǔ)更新來(lái)自其他設(shè)備的更改。令人高興的是,iOS提供了一個(gè)通知– NSPersistentStoreRemoteChange —每當(dāng)存儲(chǔ)更新發(fā)生時(shí),該通知就會(huì)發(fā)送。

再次打開(kāi)Persistence.swift并跳轉(zhuǎn)到init(inMemory :)。在PersistentContainer上調(diào)用loadPersistentStores(completionHandler :)的行之前,添加以下行:

persistentStoreDescription?.setOption(
  true as NSNumber,
  forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

添加這一行會(huì)導(dǎo)致您的存儲(chǔ)在每次更新時(shí)生成通知。

現(xiàn)在,您需要以某種方式使用此通知。 首先,向PersistenceController添加一個(gè)空方法,該方法將作為所有更新處理邏輯的占位符:

func processRemoteStoreChange(_ notification: Notification) {
  print(notification)
}

您的占位符方法只是將通知打印到Xcode控制臺(tái)。

接下來(lái),通過(guò)將其添加到init(inMemory :)的末尾,使用NotificationCenter發(fā)布者訂閱通知:

NotificationCenter.default
  .publisher(for: .NSPersistentStoreRemoteChange)
  .sink {
    self.processRemoteStoreChange($0)
  }
  .store(in: &subscriptions)

每當(dāng)您的應(yīng)用收到通知時(shí),它將調(diào)用您的新processRemoteStoreChange(_ :)

構(gòu)建并運(yùn)行,您將看到Xcode控制臺(tái)中有關(guān)每個(gè)更新的通知。 嘗試刷新火球列表,添加組,刪除火球等。 存儲(chǔ)的所有更新將生成一條通知。

那么,此通知對(duì)您有何幫助? 如果您想保持簡(jiǎn)單,則只要收到通知就可以刷新視圖上下文(view context)。 但是,有一種更智能,更高效的方法。 這就是您進(jìn)入持久性歷史記錄跟蹤(persistent history tracking)的原因。


Enabling Persistent History Tracking

如果啟用持久性歷史記錄跟蹤(persistent history tracking),則Core Data會(huì)保留持久性存儲(chǔ)中發(fā)生的所有事務(wù)的事務(wù)處理歷史記錄。 這使您可以查詢歷史記錄,以準(zhǔn)確查看更新或創(chuàng)建了哪些對(duì)象,并將僅那些更改合并到視圖上下文中。

要啟用持久性歷史記錄跟蹤,請(qǐng)將此行添加到init(inMemory :)中,緊接在PersistentContainer上調(diào)用loadPersistentStores(completionHandler :)的行之前:

persistentStoreDescription?.setOption(
  true as NSNumber, 
  forKey: NSPersistentHistoryTrackingKey)

就這些! 現(xiàn)在,該應(yīng)用程序會(huì)將每次更改的交易歷史記錄保存到您的持久性存儲(chǔ)中,您可以通過(guò)提取請(qǐng)求查詢?cè)摎v史記錄。


Making a History Request

現(xiàn)在,當(dāng)您的應(yīng)用收到存儲(chǔ)的遠(yuǎn)程更改通知時(shí),它可以查詢存儲(chǔ)的歷史記錄以發(fā)現(xiàn)更改內(nèi)容。 由于存儲(chǔ)更新可能來(lái)自多個(gè)來(lái)源,因此您將需要使用串行隊(duì)列來(lái)執(zhí)行工作。 這樣,如果同時(shí)發(fā)生多組變更,您將避免沖突或競(jìng)爭(zhēng)條件。

init(inMemory :)之前將隊(duì)列屬性添加到您的類(lèi)中

private lazy var historyRequestQueue = DispatchQueue(label: "history")

現(xiàn)在,您可以返回到processRemoteStoreChange(_ :),刪除print()語(yǔ)句并添加以下將執(zhí)行歷史記錄請(qǐng)求的代碼:

// 1
historyRequestQueue.async {
  // 2
  let backgroundContext = self.container.newBackgroundContext()
  backgroundContext.performAndWait {
    // 3
    let request = NSPersistentHistoryChangeRequest
      .fetchHistory(after: .distantPast)

    do {
      // 4
      let result = try backgroundContext.execute(request) as? 
        NSPersistentHistoryResult
      guard 
        let transactions = result?.result as? [NSPersistentHistoryTransaction],
        !transactions.isEmpty 
      else {
        return
      }
       
      // 5
      print(transactions)
    } catch {
      // log any errors
    }
  }
}

這是上面代碼中發(fā)生的事情:

  • 1) 您可以將此代碼作為歷史隊(duì)列中的一個(gè)block運(yùn)行,以串行方式處理每個(gè)通知。
  • 2) 要執(zhí)行此工作,請(qǐng)創(chuàng)建一個(gè)新的后臺(tái)上下文(background context),并使用performAndWait(_ :)在該新上下文中運(yùn)行一些代碼。
  • 3) 您可以使用NSPersistentHistoryChangeRequest.fetchHistory(after :)返回NSPersistentHistoryChangeRequest,它是NSPersistentStoreRequest的子類(lèi),可以執(zhí)行以獲取歷史交易數(shù)據(jù)。
  • 4) 您執(zhí)行請(qǐng)求,并將結(jié)果強(qiáng)制進(jìn)入NSPersistentHistoryTransaction對(duì)象數(shù)組。歷史記錄請(qǐng)求的默認(rèn)結(jié)果類(lèi)型就是這樣的對(duì)象數(shù)組。這些對(duì)象還包含NSPersistentHistoryChange對(duì)象,它們是與返回的事務(wù)相關(guān)的所有更改。
  • 5) 您將在此處處理更改?,F(xiàn)在,您只需將返回的事務(wù)打印到控制臺(tái)。

構(gòu)建并運(yùn)行并執(zhí)行常規(guī)的測(cè)試:點(diǎn)按“刷新”按鈕,刪除一些火球,然后再次刷新等等。您會(huì)發(fā)現(xiàn)通知已到達(dá),并且一系列事務(wù)對(duì)象已打印到Xcode控制臺(tái)。


Revealing a Conundrum: Big Notifications

這揭示了一個(gè)難題,如果您已經(jīng)注意到它,那就做得好!

永久存儲(chǔ)的任何更改都會(huì)觸發(fā)通知,即使您的用戶從用戶交互中添加或刪除managed object也是如此。 不僅如此:請(qǐng)注意,您的歷史記錄提取請(qǐng)求還會(huì)返回事務(wù)日志開(kāi)頭的所有更改。

您的通知也太大太多啦!

您的意圖是避免對(duì)視圖上下文(view context)進(jìn)行任何不必要的工作,控制何時(shí)刷新視圖上下文。 完全沒(méi)有問(wèn)題,您已經(jīng)覆蓋了它。 為了使整個(gè)過(guò)程清晰明了,您將通過(guò)幾個(gè)易于遵循的步驟來(lái)做到這一點(diǎn)。

1. Step 1: Setting a Query Generation

第一步 —— (邁向控制視圖上下文(view context)的一個(gè)小步驟)是設(shè)置查詢生成(query generation)。 在Persistence.swift中,將其添加到NotificationCenter發(fā)布者之前的init(inMemory :)中:

if !inMemory {
  do {
    try viewContext.setQueryGenerationFrom(.current)
  } catch {
    // log any errors  
  }
}

您將通過(guò)調(diào)用setQueryGenerationFrom(_ :)將視圖上下文固定到持久性存儲(chǔ)(persistent store)中的最新事務(wù)。 但是,由于設(shè)置query generation僅與SQLite存儲(chǔ)兼容,因此僅當(dāng)inMemoryfalse時(shí)才這樣做。

2. Step 2: Saving the History Token

您的歷史記錄請(qǐng)求使用日期來(lái)限制結(jié)果,但是有更好的方法。

NSPersistentHistoryToken是一個(gè)不透明的對(duì)象,用于標(biāo)記persistent store's transaction history中的位置。 從歷史記錄請(qǐng)求返回的每個(gè)交易對(duì)象都有一個(gè)token。 您可以存儲(chǔ)它,以便在查詢持久性歷史記錄時(shí)知道從哪里開(kāi)始。

您將需要一個(gè)屬性,用于存儲(chǔ)在應(yīng)用程序運(yùn)行時(shí)使用的token,一種將token另存為磁盤(pán)上文件的方法,以及從已保存的文件加載token的方法。

historyRequestQueue之后,將以下屬性添加到PersistenceController

private var lastHistoryToken: NSPersistentHistoryToken?

這樣會(huì)將token存儲(chǔ)在內(nèi)存中,當(dāng)然,您需要一個(gè)位置將其存儲(chǔ)在磁盤(pán)上。 接下來(lái),添加此屬性:

private lazy var tokenFileURL: URL = {
  let url = NSPersistentContainer.defaultDirectoryURL()
    .appendingPathComponent("FireballWatch", isDirectory: true)
  do {
    try FileManager.default
      .createDirectory(
        at: url, 
        withIntermediateDirectories: true, 
        attributes: nil)
  } catch {
    // log any errors
  }
  return url.appendingPathComponent("token.data", isDirectory: false)
}()

當(dāng)您第一次訪問(wèn)該屬性時(shí),tokenFileURL將嘗試創(chuàng)建存儲(chǔ)目錄。

接下來(lái),添加一種將history token作為文件保存到磁盤(pán)的方法:

private func storeHistoryToken(_ token: NSPersistentHistoryToken) {
  do {
    let data = try NSKeyedArchiver
      .archivedData(withRootObject: token, requiringSecureCoding: true)
    try data.write(to: tokenFileURL)
    lastHistoryToken = token
  } catch {
    // log any errors
  }
}

此方法將token數(shù)據(jù)存檔到磁盤(pán)上的文件中,并更新lastHistoryToken。

返回到processRemoteStoreChange(_ :)并找到以下代碼:

let request = NSPersistentHistoryChangeRequest
  .fetchHistory(after: .distantPast)

使用下面進(jìn)行替換:

let request = NSPersistentHistoryChangeRequest
  .fetchHistory(after: self.lastHistoryToken)

token的上次更新以來(lái),這僅從請(qǐng)求整個(gè)歷史變?yōu)檎?qǐng)求歷史。

接下來(lái),您可以從返回的事務(wù)數(shù)組中的最后一個(gè)事務(wù)中獲取history token并進(jìn)行存儲(chǔ)。 在print()語(yǔ)句下,添加:

if let newToken = transactions.last?.token {
  self.storeHistoryToken(newToken)
}

構(gòu)建并運(yùn)行,觀察Xcode控制臺(tái),然后點(diǎn)擊“刷新”按鈕。 第一次您應(yīng)該從頭開(kāi)始查看所有交易。 第二次您應(yīng)該看到的更少了,也許沒(méi)有。 既然您已經(jīng)下載了所有火球并存儲(chǔ)了最后的交易歷史記錄token,那么可能沒(méi)有較新的交易記錄了。

除非有新的火球發(fā)現(xiàn)!

3. Step 3: Loading the History Token

當(dāng)您的應(yīng)用啟動(dòng)時(shí),您還希望它加載最后保存的歷史token(如果存在),因此將此方法添加到PersistenceController

private func loadHistoryToken() {
  do {
    let tokenData = try Data(contentsOf: tokenFileURL)
    lastHistoryToken = try NSKeyedUnarchiver
      .unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
  } catch {
    // log any errors
  }
}

如果磁盤(pán)上的token數(shù)據(jù)存在,此方法將取消存檔,并設(shè)置lastHistoryToken屬性。

通過(guò)將其添加到init(inMemory :)的末尾來(lái)調(diào)用此方法:

loadHistoryToken()

構(gòu)建并運(yùn)行并再次查看控制臺(tái)。 不應(yīng)有新交易。 這樣,您的應(yīng)用程序便可以立即查詢歷史記錄日志!

4. Step 4: Setting a Transaction Author

您可以進(jìn)一步完善歷史記錄處理。 每個(gè)Core Data managed object context都可以設(shè)置transaction author。transaction author存儲(chǔ)在歷史記錄中,并成為一種識(shí)別每個(gè)變更來(lái)源的方法。 通過(guò)這種方式,您可以直接從后臺(tái)導(dǎo)入import過(guò)程所做的更改中分辨出用戶所做的更改。

首先,在PersistenceController的頂部,添加以下靜態(tài)屬性:

private static let authorName = "FireballWatch"
private static let remoteDataImportAuthorName = "Fireball Data Import"

這是您將用作作者名稱(chēng)的兩個(gè)靜態(tài)字符串。

注意:如果要記錄交易記錄,請(qǐng)務(wù)必有一位上下文作者,這一點(diǎn)很重要。

接下來(lái),在設(shè)置viewContext.automaticallyMergesChangesFromParent的調(diào)用的正下方添加以下內(nèi)容到init(inMemory :)行中:

viewContext.transactionAuthor = PersistenceController.authorName

這將使用您剛創(chuàng)建的靜態(tài)屬性設(shè)置view contexttransaction author。

接下來(lái),向下滾動(dòng)至batchInsertFireballs(_ :),然后在傳遞給performBackgroundTask(_ :)的閉包內(nèi),在開(kāi)頭添加以下行:

context.transactionAuthor = PersistenceController.remoteDataImportAuthorName

這會(huì)將用于將數(shù)據(jù)導(dǎo)入到其他靜態(tài)屬性的后臺(tái)上下文的transaction author設(shè)置。 因此,現(xiàn)在根據(jù)對(duì)上下文的更改記錄的歷史記錄將具有可識(shí)別的來(lái)源,而且重要的是,它不同于用于UI更新的transaction author,例如通過(guò)滑動(dòng)行進(jìn)行刪除。

5. Step 5: Creating a History Request Predicate

要過(guò)濾掉由用戶引起的任何交易,您需要添加帶有謂詞的提取請(qǐng)求。

找到processRemoteStoreChange(_ :)并在執(zhí)行do之前添加以下內(nèi)容:

if let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest {
  historyFetchRequest.predicate = 
    NSPredicate(format: "%K != %@", "author", PersistenceController.authorName)
  request.fetchRequest = historyFetchRequest
}

首先,使用類(lèi)屬性NSPersistentHistoryTransaction.fetchRequest創(chuàng)建一個(gè)NSFetchRequest并設(shè)置其謂詞。 如果transaction author不是您創(chuàng)建的用于識(shí)別用戶交易的字符串,則謂詞測(cè)試將返回true。 然后,使用此謂詞獲取請(qǐng)求設(shè)置NSPersistentHistoryChangeRequestfetchRequest屬性。

構(gòu)建并運(yùn)行,并觀察控制臺(tái)。 您將看到所有這些工作的結(jié)果。 刪除一個(gè)火球,您將看不到任何打印到控制臺(tái)的交易,因?yàn)槟谥苯舆^(guò)濾掉由用戶生成的交易。 但是,如果您隨后點(diǎn)擊刷新按鈕,則會(huì)看到出現(xiàn)一個(gè)新事務(wù),因?yàn)檫@是批導(dǎo)入添加的新記錄。 成功!

那是一個(gè)漫長(zhǎng)的過(guò)程-您最近好嗎? 在這些艱難時(shí)期,記住您應(yīng)用程序的核心使命始終是一件好事:拯救人類(lèi)免受外來(lái)入侵。 都值得!

6. Step 6: Merging Important Changes

好的,您已經(jīng)添加了所有必要的優(yōu)化,以確保您的視圖上下文(view context)流程僅從最相關(guān)的事務(wù)中進(jìn)行更改。 剩下要做的就是將這些更改合并到視圖上下文中以更新UI。 這是相對(duì)簡(jiǎn)單的。

將以下方法添加到您的PersistenceController

private func mergeChanges(from transactions: [NSPersistentHistoryTransaction]) {
  let context = viewContext
  // 1
  context.perform {
    // 2
    transactions.forEach { transaction in
      // 3
      guard let userInfo = transaction.objectIDNotification().userInfo else {
        return
      }

      // 4
      NSManagedObjectContext
        .mergeChanges(fromRemoteContextSave: userInfo, into: [context])
    }
  }
}

這是上面代碼中發(fā)生的事情:

  • 1) 您確保使用perform(_ :)在視圖上下文的隊(duì)列上進(jìn)行工作。
  • 2) 您遍歷傳遞給此方法的每個(gè)事務(wù)。
  • 3) 每個(gè)事務(wù)都包含每個(gè)更改的所有詳細(xì)信息,但是您需要以可傳遞給mergeChanges(fromRemoteContextSave:into :)的形式使用它:一個(gè)userInfo字典。 objectIDNotification().userInfo只是您需要的字典。
  • 4) 將其傳遞給mergeChanges(fromRemoteContextSave:into :)將使視圖上下文與事務(wù)更改保持最新。

還記得您之前設(shè)置的query generation嗎? mergeChanges(fromRemoteContextSave:into :)方法的作用之一是更新上下文的query generation。

剩下的就是調(diào)用您的新方法。 在調(diào)用print(_ :)之前,將以下行添加到processRemoteStoreChange(_:)(如果需要,您也可以刪除對(duì)print(_ :)的調(diào)用?。?/p>

self.mergeChanges(from: transactions)

現(xiàn)在,流程更改方法將過(guò)濾事務(wù),并將僅最相關(guān)的事務(wù)傳遞給mergeChanges(from :)方法。

構(gòu)建并運(yùn)行!

忘記控制臺(tái),簽出您的應(yīng)用程序。 刷新兩次,第二次您什么也看不到,因?yàn)椴恍枰魏喂ぷ鳌?然后,刪除一個(gè)火球,然后點(diǎn)擊刷新按鈕。 您會(huì)看到它再次出現(xiàn)!


Adding Derived Attributes

您可以將火球添加到組中,因此最好在組列表中顯示火球計(jì)數(shù)。

派生屬性是Core Data的最新添加,允許您創(chuàng)建一個(gè)實(shí)體屬性,該實(shí)體屬性是在每次將上下文保存并存儲(chǔ)到持久性存儲(chǔ)區(qū)時(shí)從子entity數(shù)據(jù)計(jì)算得出的。 這使它高效,因?yàn)槟槐卦诿看巫x取時(shí)都重新計(jì)算它。

您在managed object model中創(chuàng)建派生屬性。 打開(kāi)FireballWatch.xcdatamodeld,然后選擇FireballGroup entity。 找到Attributes部分,然后單擊加號(hào)按鈕以添加新屬性。 將其稱(chēng)為fireballCount并將類(lèi)型設(shè)置為Integer 64。

在右側(cè)的Data Model inspector中,選中Derived復(fù)選框,其中將顯示Derivation字段。 在此字段中,鍵入以下內(nèi)容:

fireballs.@count

這使用謂詞聚合函數(shù)@count并作用于現(xiàn)有的fireballs關(guān)系以返回該組的child entities有多少個(gè)火球的計(jì)數(shù)。

記住要保存您的managed object model。

注意:從Xcode 12開(kāi)始,派生屬性僅限于一些特定的用例。 您可以find out what's possible in the Apple documentation。

剩下要做的就是顯示計(jì)數(shù)。

打開(kāi)View group中的FireballGroupList.swift,找到以下行:

Text("\(group.name ?? "Untitled")")

替換成下面的:

HStack {
  Text("\(group.name ?? "Untitled")")
  Spacer()
  Image(systemName: "sun.max.fill")
  Text("\(group.fireballCount)")
}

這只是向每行添加一個(gè)圖標(biāo)和火球計(jì)數(shù)。 構(gòu)建并運(yùn)行以查看其顯示方式:

Perfect!

如果您正在尋找挑戰(zhàn),請(qǐng)嘗試添加代碼以在處理完不必要的交易記錄后將其刪除,以免歷史記錄無(wú)限期地增長(zhǎng)。 有一個(gè)方便的工作方法:NSPersistentHistoryChangeRequest.deleteHistoryBefore(_ :)

如果您想進(jìn)一步了解Core Data,建議您:

后記

本篇主要講述了基于批插入和存儲(chǔ)歷史等高效CoreData使用示例,感興趣的給個(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)容