数据持久化方案解析(十九) —— 基于批插入和存储历史等高效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使用示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容