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

版本記錄

版本號 時間
V1.0 2020.12.10 星期四

前言

數(shù)據(jù)的持久化存儲是移動端不可避免的一個問題蛋褥,很多時候的業(yè)務(wù)邏輯都需要我們進行本地化存儲解決和完成耻蛇,我們可以采用很多持久化存儲方案冕末,比如說plist文件(屬性列表)憋肖、preference(偏好設(shè)置)朝群、NSKeyedArchiver(歸檔)姜盈、SQLite 3CoreData猪腕,這里基本上我們都用過冗澈。這幾種方案各有優(yōu)缺點,其中陋葡,CoreData是蘋果極力推薦我們使用的一種方式亚亲,我已經(jīng)將它分離出去一個專題進行說明講解。這個專題主要就是針對另外幾種數(shù)據(jù)持久化存儲方案而設(shè)立脖岛。
1. 數(shù)據(jù)持久化方案解析(一) —— 一個簡單的基于SQLite持久化方案示例(一)
2. 數(shù)據(jù)持久化方案解析(二) —— 一個簡單的基于SQLite持久化方案示例(二)
3. 數(shù)據(jù)持久化方案解析(三) —— 基于NSCoding的持久化存儲(一)
4. 數(shù)據(jù)持久化方案解析(四) —— 基于NSCoding的持久化存儲(二)
5. 數(shù)據(jù)持久化方案解析(五) —— 基于Realm的持久化存儲(一)
6. 數(shù)據(jù)持久化方案解析(六) —— 基于Realm的持久化存儲(二)
7. 數(shù)據(jù)持久化方案解析(七) —— 基于Realm的持久化存儲(三)
8. 數(shù)據(jù)持久化方案解析(八) —— UIDocument的數(shù)據(jù)存儲(一)
9. 數(shù)據(jù)持久化方案解析(九) —— UIDocument的數(shù)據(jù)存儲(二)
10. 數(shù)據(jù)持久化方案解析(十) —— UIDocument的數(shù)據(jù)存儲(三)
11. 數(shù)據(jù)持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲示例(一)
12. 數(shù)據(jù)持久化方案解析(十二) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲示例(二)
13. 數(shù)據(jù)持久化方案解析(十三) —— 基于Unit Testing的Core Data測試(一)
14. 數(shù)據(jù)持久化方案解析(十四) —— 基于Unit Testing的Core Data測試(二)
15. 數(shù)據(jù)持久化方案解析(十五) —— 基于Realm和SwiftUI的數(shù)據(jù)持久化簡單示例(一)
16. 數(shù)據(jù)持久化方案解析(十六) —— 基于Realm和SwiftUI的數(shù)據(jù)持久化簡單示例(二)
17. 數(shù)據(jù)持久化方案解析(十七) —— 基于NSPersistentCloudKitContainer的Core Data和CloudKit的集成示例(一)
18. 數(shù)據(jù)持久化方案解析(十八) —— 基于NSPersistentCloudKitContainer的Core Data和CloudKit的集成示例(二)

開始

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

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

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

Swift 5, iOS 14, Xcode 12

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

Core Data是已存在很長時間的古老的Apple框架之一绍在。自從iOS 10中發(fā)布NSPersistentContainer以來,蘋果公司就向Core Data表示了極大的熱愛。最新添加的Core Data進一步提升了其競爭力〕ザ桑現(xiàn)在有批量插入請求臼寄,持久性歷史記錄和派生屬性,這些絕對可以使Core Data的使用效率更高溜宽。

在本教程中吉拳,您將通過提高數(shù)據(jù)存儲效率來改進應(yīng)用程序。您將學習如何:

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

您可能會在此過程中拯救人類适揉!

注意:本中級教程假定您具有使用Xcode編寫iOS應(yīng)用程序和編寫Swift的經(jīng)驗留攒。您應(yīng)該已經(jīng)使用過Core Data,并對其概念感到滿意嫉嘀。如果您想學習基礎(chǔ)知識炼邀,可以先嘗試Core Data with SwiftUI tutorial

Fireballs剪侮!他們無處不在拭宁!有人在注意嗎?Fireballs可能是外星人入侵的最初跡象瓣俯,也可能是即將來臨的大決戰(zhàn)的預(yù)兆杰标。有人必須保持警惕。這是你的任務(wù)彩匕。您已經(jīng)制作了一個應(yīng)用程序腔剂,可以從NASA Jet Propulsion Laboratory (JPL)下載火球瞄準點,以便將它們分組并報告可疑的火球活動推掸。

打開啟動項目桶蝎。 看你到目前為止有什么。


Exploring Fireball Watch

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

啟動時,列表將為空仇味,因此請點擊Fireballs列表右上角的刷新按鈕呻顽。 很快,該列表就會填滿丹墨。 您可以再次點擊以查看它沒有為相同數(shù)據(jù)添加重復(fù)記錄廊遍。 如果您在某些火球單元上向左滑動并刪除了一些,然后再次點擊刷新贩挣,則會看到下載數(shù)據(jù)后重新創(chuàng)建的那些fireballs喉前。

如果點擊Groups選項卡没酣,則可以添加一個組。 進行一些分組卵迂,然后返回Fireballs選項卡裕便,然后在列表中點擊一個火球。 然后见咒,點擊右上角的in-tray按鈕以選擇一個或多個包含該火球的組偿衰。 當您點擊Groups標簽中列出的組列表時,它將向您顯示那個組中所有火球的地圖改览。

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


Examining the Core Data Stack

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

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

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

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

importFetchedFireballs(_ :)循環(huán)遍歷FireballData數(shù)組推励,創(chuàng)建一個managed object并保存managed object context鹤耍。 由于永久性容器的viewContextautomaticallyMergesChangesFromParent設(shè)置為true,因此在應(yīng)用程序保存所有對象時验辞,這可能會使UI停滯稿黄。 這是一個會使應(yīng)用感覺很笨拙的問題,是您第一次改進的目標跌造。


Making a Batch Insert Request

報告的火球列表只會越來越大杆怕,如果突然出現(xiàn)火球群怎么辦? 火球群可能表明可能有外星人著陸點壳贪,預(yù)示著新的入侵嘗試陵珍!

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

批量插入可助您一臂之力磕蒲! 批處理插入請求是一種特殊的持久性存儲請求伟姐,它允許您將大量數(shù)據(jù)直接導(dǎo)入到持久性存儲中收苏。 您需要一個方法來為此操作創(chuàng)建批量插入請求亿卤。 打開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對象數(shù)組愤兵,并創(chuàng)建一個NSBatchInsertRequest來插入所有對象。就是這樣:

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

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


Batch Inserting Fireballs

這樣就完成了請求創(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
    }
  }
}

下面進行細分:

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

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

self?.batchInsertFireballs($0)

您也可以注釋或刪除importFetchedFireballs(_ :)刚操,因為不再需要它闸翅。

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

剩下要做的就是構(gòu)建并運行记某!

但是您可能會注意到有些問題。 如果刪除火球华蜒,然后再次點擊刷新按鈕辙纬,則列表不會更新。 那是因為批處理插入請求將數(shù)據(jù)插入到持久性存儲(persistent store)中叭喜,但是視圖上下文(view context)沒有更新贺拣,因此它不知道任何更改。 您可以通過重啟應(yīng)用來確認這一點捂蕴,然后您將看到所有新數(shù)據(jù)現(xiàn)在都顯示在列表中譬涡。

以前,您是在后臺隊列上下文(background queue context)中創(chuàng)建對象并保存上下文啥辨,這會將更改推送到持久性存儲協(xié)調(diào)器(persistent store coordinator)涡匀。保存后臺上下文后,它已從持久性存儲協(xié)調(diào)器自動更新溉知,因為您已在視圖上下文中將automaticallyMergeChangesFromParent設(shè)置為true陨瘩。

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


Enabling Notifications

當然甚淡,在后臺更新存儲并非不常見。例如捅厂,您可能具有一個用于擴展持久性存儲(persistent store)的應(yīng)用程序擴展贯卦,或者您的應(yīng)用程序支持iCloud资柔,并且您的應(yīng)用程序的存儲更新來自其他設(shè)備的更改。令人高興的是撵割,iOS提供了一個通知– NSPersistentStoreRemoteChange —每當存儲更新發(fā)生時贿堰,該通知就會發(fā)送。

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

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

添加這一行會導(dǎo)致您的存儲在每次更新時生成通知励两。

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

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

您的占位符方法只是將通知打印到Xcode控制臺嘴高。

接下來浸策,通過將其添加到init(inMemory :)的末尾捐晶,使用NotificationCenter發(fā)布者訂閱通知:

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

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

構(gòu)建并運行惑灵,您將看到Xcode控制臺中有關(guān)每個更新的通知山上。 嘗試刷新火球列表,添加組英支,刪除火球等佩憾。 存儲的所有更新將生成一條通知。

那么干花,此通知對您有何幫助妄帘? 如果您想保持簡單,則只要收到通知就可以刷新視圖上下文(view context)池凄。 但是抡驼,有一種更智能,更高效的方法肿仑。 這就是您進入持久性歷史記錄跟蹤(persistent history tracking)的原因致盟。


Enabling Persistent History Tracking

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

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

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

就這些荔泳! 現(xiàn)在蕉饼,該應(yīng)用程序會將每次更改的交易歷史記錄保存到您的持久性存儲中虐杯,您可以通過提取請求查詢該歷史記錄。


Making a History Request

現(xiàn)在昧港,當您的應(yīng)用收到存儲的遠程更改通知時擎椰,它可以查詢存儲的歷史記錄以發(fā)現(xiàn)更改內(nèi)容。 由于存儲更新可能來自多個來源创肥,因此您將需要使用串行隊列來執(zhí)行工作达舒。 這樣,如果同時發(fā)生多組變更叹侄,您將避免沖突或競爭條件巩搏。

init(inMemory :)之前將隊列屬性添加到您的類中

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

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

// 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) 您可以將此代碼作為歷史隊列中的一個block運行贯底,以串行方式處理每個通知。
  • 2) 要執(zhí)行此工作撒强,請創(chuàng)建一個新的后臺上下文(background context)禽捆,并使用performAndWait(_ :)在該新上下文中運行一些代碼。
  • 3) 您可以使用NSPersistentHistoryChangeRequest.fetchHistory(after :)返回NSPersistentHistoryChangeRequest飘哨,它是NSPersistentStoreRequest的子類胚想,可以執(zhí)行以獲取歷史交易數(shù)據(jù)。
  • 4) 您執(zhí)行請求芽隆,并將結(jié)果強制進入NSPersistentHistoryTransaction對象數(shù)組浊服。歷史記錄請求的默認結(jié)果類型就是這樣的對象數(shù)組。這些對象還包含NSPersistentHistoryChange對象摆马,它們是與返回的事務(wù)相關(guān)的所有更改臼闻。
  • 5) 您將在此處處理更改。現(xiàn)在囤采,您只需將返回的事務(wù)打印到控制臺述呐。

構(gòu)建并運行并執(zhí)行常規(guī)的測試:點按“刷新”按鈕,刪除一些火球蕉毯,然后再次刷新等等乓搬。您會發(fā)現(xiàn)通知已到達,并且一系列事務(wù)對象已打印到Xcode控制臺代虾。


Revealing a Conundrum: Big Notifications

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

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

您的通知也太大太多啦策泣!

您的意圖是避免對視圖上下文(view context)進行任何不必要的工作,控制何時刷新視圖上下文抬吟。 完全沒有問題萨咕,您已經(jīng)覆蓋了它。 為了使整個過程清晰明了火本,您將通過幾個易于遵循的步驟來做到這一點危队。

1. Step 1: Setting a Query Generation

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

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

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

2. Step 2: Saving the History Token

您的歷史記錄請求使用日期來限制結(jié)果,但是有更好的方法叔锐。

NSPersistentHistoryToken是一個不透明的對象,用于標記persistent store's transaction history中的位置见秽。 從歷史記錄請求返回的每個交易對象都有一個token愉烙。 您可以存儲它,以便在查詢持久性歷史記錄時知道從哪里開始解取。

您將需要一個屬性步责,用于存儲在應(yīng)用程序運行時使用的token,一種將token另存為磁盤上文件的方法禀苦,以及從已保存的文件加載token的方法蔓肯。

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

private var lastHistoryToken: NSPersistentHistoryToken?

這樣會將token存儲在內(nè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)
}()

當您第一次訪問該屬性時调限,tokenFileURL將嘗試創(chuàng)建存儲目錄。

接下來误澳,添加一種將history token作為文件保存到磁盤的方法:

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ù)存檔到磁盤上的文件中耻矮,并更新lastHistoryToken

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

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

使用下面進行替換:

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

token的上次更新以來忆谓,這僅從請求整個歷史變?yōu)檎埱髿v史裆装。

接下來,您可以從返回的事務(wù)數(shù)組中的最后一個事務(wù)中獲取history token并進行存儲。 在print()語句下哨免,添加:

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

構(gòu)建并運行茎活,觀察Xcode控制臺,然后點擊“刷新”按鈕铁瞒。 第一次您應(yīng)該從頭開始查看所有交易妙色。 第二次您應(yīng)該看到的更少了,也許沒有慧耍。 既然您已經(jīng)下載了所有火球并存儲了最后的交易歷史記錄token身辨,那么可能沒有較新的交易記錄了。

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

3. Step 3: Loading the History Token

當您的應(yīng)用啟動時煌珊,您還希望它加載最后保存的歷史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
  }
}

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

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

loadHistoryToken()

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

4. Step 4: Setting a Transaction Author

您可以進一步完善歷史記錄處理畴博。 每個Core Data managed object context都可以設(shè)置transaction authortransaction author存儲在歷史記錄中蓝仲,并成為一種識別每個變更來源的方法俱病。 通過這種方式,您可以直接從后臺導(dǎo)入import過程所做的更改中分辨出用戶所做的更改袱结。

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

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

這是您將用作作者名稱的兩個靜態(tài)字符串垢夹。

注意:如果要記錄交易記錄溢吻,請務(wù)必有一位上下文作者,這一點很重要棚饵。

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

viewContext.transactionAuthor = PersistenceController.authorName

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

接下來噪漾,向下滾動至batchInsertFireballs(_ :)硼砰,然后在傳遞給performBackgroundTask(_ :)的閉包內(nèi),在開頭添加以下行:

context.transactionAuthor = PersistenceController.remoteDataImportAuthorName

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

5. Step 5: Creating a History Request Predicate

要過濾掉由用戶引起的任何交易,您需要添加帶有謂詞的提取請求血公。

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

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

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

構(gòu)建并運行吕世,并觀察控制臺。 您將看到所有這些工作的結(jié)果梯投。 刪除一個火球命辖,您將看不到任何打印到控制臺的交易,因為您正在直接過濾掉由用戶生成的交易分蓖。 但是尔艇,如果您隨后點擊刷新按鈕,則會看到出現(xiàn)一個新事務(wù)么鹤,因為這是批導(dǎo)入添加的新記錄漓帚。 成功!

那是一個漫長的過程-您最近好嗎午磁? 在這些艱難時期,記住您應(yīng)用程序的核心使命始終是一件好事:拯救人類免受外來入侵毡们。 都值得迅皇!

6. Step 6: Merging Important Changes

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

將以下方法添加到您的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(_ :)在視圖上下文的隊列上進行工作框咙。
  • 2) 您遍歷傳遞給此方法的每個事務(wù)。
  • 3) 每個事務(wù)都包含每個更改的所有詳細信息痢甘,但是您需要以可傳遞給mergeChanges(fromRemoteContextSave:into :)的形式使用它:一個userInfo字典喇嘱。 objectIDNotification().userInfo只是您需要的字典。
  • 4) 將其傳遞給mergeChanges(fromRemoteContextSave:into :)將使視圖上下文與事務(wù)更改保持最新塞栅。

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

剩下的就是調(diào)用您的新方法。 在調(diào)用print(_ :)之前作烟,將以下行添加到processRemoteStoreChange(_:)(如果需要愉粤,您也可以刪除對print(_ :)的調(diào)用!):

self.mergeChanges(from: transactions)

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

構(gòu)建并運行压恒!

忘記控制臺影暴,簽出您的應(yīng)用程序。 刷新兩次涎显,第二次您什么也看不到坤检,因為不需要任何工作。 然后期吓,刪除一個火球早歇,然后點擊刷新按鈕。 您會看到它再次出現(xiàn)讨勤!


Adding Derived Attributes

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

派生屬性是Core Data的最新添加潭千,允許您創(chuàng)建一個實體屬性谱姓,該實體屬性是在每次將上下文保存并存儲到持久性存儲區(qū)時從子entity數(shù)據(jù)計算得出的。 這使它高效刨晴,因為您不必在每次讀取時都重新計算它屉来。

您在managed object model中創(chuàng)建派生屬性。 打開FireballWatch.xcdatamodeld狈癞,然后選擇FireballGroup entity茄靠。 找到Attributes部分,然后單擊加號按鈕以添加新屬性蝶桶。 將其稱為fireballCount并將類型設(shè)置為Integer 64慨绳。

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

fireballs.@count

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

記住要保存您的managed object model战秋。

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

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

打開View group中的FireballGroupList.swift,找到以下行:

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

替換成下面的:

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

這只是向每行添加一個圖標和火球計數(shù)吉嚣。 構(gòu)建并運行以查看其顯示方式:

Perfect!

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

如果您想進一步了解Core Data,建議您:

后記

本篇主要講述了基于批插入和存儲歷史等高效CoreData使用示例秋泄,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末琐馆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恒序,更是在濱河造成了極大的恐慌瘦麸,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件歧胁,死亡現(xiàn)場離奇詭異滋饲,居然都是意外死亡,警方通過查閱死者的電腦和手機喊巍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門屠缭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人崭参,你說我怎么就攤上這事呵曹。” “怎么了何暮?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵奄喂,是天一觀的道長。 經(jīng)常有香客問我海洼,道長砍聊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任贰军,我火速辦了婚禮,結(jié)果婚禮上蟹肘,老公的妹妹穿的比我還像新娘词疼。我一直安慰自己,他們只是感情好帘腹,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布贰盗。 她就那樣靜靜地躺著,像睡著了一般阳欲。 火紅的嫁衣襯著肌膚如雪舵盈。 梳的紋絲不亂的頭發(fā)上陋率,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音秽晚,去河邊找鬼瓦糟。 笑死,一個胖子當著我的面吹牛赴蝇,可吹牛的內(nèi)容都是我干的菩浙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼句伶,長吁一口氣:“原來是場噩夢啊……” “哼劲蜻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起考余,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤先嬉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后楚堤,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疫蔓,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年钾军,在試婚紗的時候發(fā)現(xiàn)自己被綠了鳄袍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡吏恭,死狀恐怖拗小,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情樱哼,我是刑警寧澤哀九,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站搅幅,受9級特大地震影響阅束,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茄唐,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一息裸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沪编,春花似錦呼盆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至相嵌,卻和暖如春腿时,著一層夾襖步出監(jiān)牢的瞬間况脆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工批糟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留格了,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓跃赚,卻偏偏與公主長得像笆搓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子纬傲,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容