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

版本記錄

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

前言

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

開始

首先看下主要內容:

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

下面看下寫作環境:

Swift 5, iOS 14, Xcode 12

接著就是主要內容了。

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

在本教程中,您將通過提高數據存儲效率來改進應用程序。您將學習如何:

  • 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應用程序和編寫Swift的經驗。您應該已經使用過Core Data,并對其概念感到滿意。如果您想學習基礎知識,可以先嘗試Core Data with SwiftUI tutorial

Fireballs!他們無處不在!有人在注意嗎?Fireballs可能是外星人入侵的最初跡象,也可能是即將來臨的大決戰的預兆。有人必須保持警惕。這是你的任務。您已經制作了一個應用程序,可以從NASA Jet Propulsion Laboratory (JPL)下載火球瞄準點,以便將它們分組并報告可疑的火球活動。

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


Exploring Fireball Watch

構建并運行該應用程序,以便您可以了解其工作方式。 該應用程序從JPL下載最新的火球數據,為每個火球瞄準創建記錄并將其存儲在Core Data stack中。 您還可以創建組并將火球添加到組中以進行報告。

啟動時,列表將為空,因此請點擊Fireballs列表右上角的刷新按鈕。 很快,該列表就會填滿。 您可以再次點擊以查看它沒有為相同數據添加重復記錄。 如果您在某些火球單元上向左滑動并刪除了一些,然后再次點擊刷新,則會看到下載數據后重新創建的那些fireballs

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

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


Examining the Core Data Stack

現在,看看應用程序的Core Data stack是如何設置的。

打開Persistence.swift。 您會看到一個名為PersistenceController的類。 此類處理您的所有Core Data設置和數據導入。 它使用NSPersistentContainer創建一個標準的SQLite存儲,或者創建一個用于SwiftUI預覽的內存存儲。

persistent containerviewContext是應用程序用于獲取請求(生成列表數據)的managed object context。 這是典型的設置。 您的模型中有兩個實體(entities)FireballFireballGroup

PersistenceController具有fetchFireballs(),可下載火球數據并調用私有importFetchedFireballs(_ :)以將所得的FireballData struct數組導入為Fireballmanaged objects。 它使用持久性容器的performBackgroundTask(_ :)作為后臺任務來執行此操作。

importFetchedFireballs(_ :)循環遍歷FireballData數組,創建一個managed object并保存managed object context。 由于永久性容器的viewContextautomaticallyMergesChangesFromParent設置為true,因此在應用程序保存所有對象時,這可能會使UI停滯。 這是一個會使應用感覺很笨拙的問題,是您第一次改進的目標。


Making a Batch Insert Request

報告的火球列表只會越來越大,如果突然出現火球群怎么辦? 火球群可能表明可能有外星人著陸點,預示著新的入侵嘗試!

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

批量插入可助您一臂之力! 批處理插入請求是一種特殊的持久性存儲請求,它允許您將大量數據直接導入到持久性存儲中。 您需要一個方法來為此操作創建批量插入請求。 打開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對象數組,并創建一個NSBatchInsertRequest來插入所有對象。就是這樣:

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

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


Batch Inserting Fireballs

這樣就完成了請求創建。 現在,您如何使用它? 將以下方法添加到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) 首先,請檢查是否有實際的工作要做,以確保數組不為空。
  • 2) 然后要求PersistentContainer使用performBackgroundTask(_ :)執行后臺任務。
  • 3) 創建批處理插入請求,然后執行它,捕獲可能引發的任何錯誤。 批處理請求通過一次事務將所有數據插入持久性存儲(persistent store)中。 由于您的Core Data model已定義了唯一約束,因此它將僅創建不存在的新記錄,并在需要時更新現有記錄。

最后一項更改:轉到fetchFireballs(),而不是調用self?.importFetchedFireballs($ 0),將其更改為:

self?.batchInsertFireballs($0)

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

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

剩下要做的就是構建并運行!

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

以前,您是在后臺隊列上下文(background queue context)中創建對象并保存上下文,這會將更改推送到持久性存儲協調器(persistent store coordinator)。保存后臺上下文后,它已從持久性存儲協調器自動更新,因為您已在視圖上下文中將automaticallyMergeChangesFromParent設置為true

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


Enabling Notifications

當然,在后臺更新存儲并非不常見。例如,您可能具有一個用于擴展持久性存儲(persistent store)的應用程序擴展,或者您的應用程序支持iCloud,并且您的應用程序的存儲更新來自其他設備的更改。令人高興的是,iOS提供了一個通知– NSPersistentStoreRemoteChange —每當存儲更新發生時,該通知就會發送。

再次打開Persistence.swift并跳轉到init(inMemory :)。在PersistentContainer上調用loadPersistentStores(completionHandler :)的行之前,添加以下行:

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

添加這一行會導致您的存儲在每次更新時生成通知。

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

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

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

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

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

每當您的應用收到通知時,它將調用您的新processRemoteStoreChange(_ :)

構建并運行,您將看到Xcode控制臺中有關每個更新的通知。 嘗試刷新火球列表,添加組,刪除火球等。 存儲的所有更新將生成一條通知。

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


Enabling Persistent History Tracking

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

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

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

就這些! 現在,該應用程序會將每次更改的交易歷史記錄保存到您的持久性存儲中,您可以通過提取請求查詢該歷史記錄。


Making a History Request

現在,當您的應用收到存儲的遠程更改通知時,它可以查詢存儲的歷史記錄以發現更改內容。 由于存儲更新可能來自多個來源,因此您將需要使用串行隊列來執行工作。 這樣,如果同時發生多組變更,您將避免沖突或競爭條件。

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

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

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

// 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
    }
  }
}

這是上面代碼中發生的事情:

  • 1) 您可以將此代碼作為歷史隊列中的一個block運行,以串行方式處理每個通知。
  • 2) 要執行此工作,請創建一個新的后臺上下文(background context),并使用performAndWait(_ :)在該新上下文中運行一些代碼。
  • 3) 您可以使用NSPersistentHistoryChangeRequest.fetchHistory(after :)返回NSPersistentHistoryChangeRequest,它是NSPersistentStoreRequest的子類,可以執行以獲取歷史交易數據。
  • 4) 您執行請求,并將結果強制進入NSPersistentHistoryTransaction對象數組。歷史記錄請求的默認結果類型就是這樣的對象數組。這些對象還包含NSPersistentHistoryChange對象,它們是與返回的事務相關的所有更改。
  • 5) 您將在此處處理更改。現在,您只需將返回的事務打印到控制臺。

構建并運行并執行常規的測試:點按“刷新”按鈕,刪除一些火球,然后再次刷新等等。您會發現通知已到達,并且一系列事務對象已打印到Xcode控制臺。


Revealing a Conundrum: Big Notifications

這揭示了一個難題,如果您已經注意到它,那就做得好!

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

您的通知也太大太多啦!

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

1. Step 1: Setting a Query Generation

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

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

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

2. Step 2: Saving the History Token

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

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

您將需要一個屬性,用于存儲在應用程序運行時使用的token,一種將token另存為磁盤上文件的方法,以及從已保存的文件加載token的方法。

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

private var lastHistoryToken: NSPersistentHistoryToken?

這樣會將token存儲在內存中,當然,您需要一個位置將其存儲在磁盤上。 接下來,添加此屬性:

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將嘗試創建存儲目錄。

接下來,添加一種將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數據存檔到磁盤上的文件中,并更新lastHistoryToken

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

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

使用下面進行替換:

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

token的上次更新以來,這僅從請求整個歷史變為請求歷史。

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

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

構建并運行,觀察Xcode控制臺,然后點擊“刷新”按鈕。 第一次您應該從頭開始查看所有交易。 第二次您應該看到的更少了,也許沒有。 既然您已經下載了所有火球并存儲了最后的交易歷史記錄token,那么可能沒有較新的交易記錄了。

除非有新的火球發現!

3. Step 3: Loading the History Token

當您的應用啟動時,您還希望它加載最后保存的歷史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數據存在,此方法將取消存檔,并設置lastHistoryToken屬性。

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

loadHistoryToken()

構建并運行并再次查看控制臺。 不應有新交易。 這樣,您的應用程序便可以立即查詢歷史記錄日志!

4. Step 4: Setting a Transaction Author

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

首先,在PersistenceController的頂部,添加以下靜態屬性:

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

這是您將用作作者名稱的兩個靜態字符串。

注意:如果要記錄交易記錄,請務必有一位上下文作者,這一點很重要。

接下來,在設置viewContext.automaticallyMergesChangesFromParent的調用的正下方添加以下內容到init(inMemory :)行中:

viewContext.transactionAuthor = PersistenceController.authorName

這將使用您剛創建的靜態屬性設置view contexttransaction author

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

context.transactionAuthor = PersistenceController.remoteDataImportAuthorName

這會將用于將數據導入到其他靜態屬性的后臺上下文的transaction author設置。 因此,現在根據對上下文的更改記錄的歷史記錄將具有可識別的來源,而且重要的是,它不同于用于UI更新的transaction author,例如通過滑動行進行刪除。

5. Step 5: Creating a History Request Predicate

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

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

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

首先,使用類屬性NSPersistentHistoryTransaction.fetchRequest創建一個NSFetchRequest并設置其謂詞。 如果transaction author不是您創建的用于識別用戶交易的字符串,則謂詞測試將返回true。 然后,使用此謂詞獲取請求設置NSPersistentHistoryChangeRequestfetchRequest屬性。

構建并運行,并觀察控制臺。 您將看到所有這些工作的結果。 刪除一個火球,您將看不到任何打印到控制臺的交易,因為您正在直接過濾掉由用戶生成的交易。 但是,如果您隨后點擊刷新按鈕,則會看到出現一個新事務,因為這是批導入添加的新記錄。 成功!

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

6. Step 6: Merging Important Changes

好的,您已經添加了所有必要的優化,以確保您的視圖上下文(view context)流程僅從最相關的事務中進行更改。 剩下要做的就是將這些更改合并到視圖上下文中以更新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])
    }
  }
}

這是上面代碼中發生的事情:

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

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

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

self.mergeChanges(from: transactions)

現在,流程更改方法將過濾事務,并將僅最相關的事務傳遞給mergeChanges(from :)方法。

構建并運行!

忘記控制臺,簽出您的應用程序。 刷新兩次,第二次您什么也看不到,因為不需要任何工作。 然后,刪除一個火球,然后點擊刷新按鈕。 您會看到它再次出現!


Adding Derived Attributes

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

派生屬性是Core Data的最新添加,允許您創建一個實體屬性,該實體屬性是在每次將上下文保存并存儲到持久性存儲區時從子entity數據計算得出的。 這使它高效,因為您不必在每次讀取時都重新計算它。

您在managed object model中創建派生屬性。 打開FireballWatch.xcdatamodeld,然后選擇FireballGroup entity。 找到Attributes部分,然后單擊加號按鈕以添加新屬性。 將其稱為fireballCount并將類型設置為Integer 64

在右側的Data Model inspector中,選中Derived復選框,其中將顯示Derivation字段。 在此字段中,鍵入以下內容:

fireballs.@count

這使用謂詞聚合函數@count并作用于現有的fireballs關系以返回該組的child entities有多少個火球的計數。

記住要保存您的managed object model

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

剩下要做的就是顯示計數。

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

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

替換成下面的:

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

這只是向每行添加一個圖標和火球計數。 構建并運行以查看其顯示方式:

Perfect!

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

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

后記

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容