跳转至

4.中间取数

在本书的前三章中,您开始探索Core Data的基础,包括在Core Data持久性存储中保存和获取数据的非常基本的方法。

到目前为止,您已经执行了大多数简单的、未经改进的获取,例如“获取所有BowTie实体。“ 有时这就是你需要做的一切。 通常,您希望对如何从Core Data检索信息施加更多控制。

基于你到目前为止所学到的知识,本章将深入探讨fetching的主题。 获取是Core Data中的一个大主题,您可以使用许多工具。 在本章结束时,您将了解如何:

  • 只取你需要的东西
  • 使用谓词优化获取的结果
  • 在后台提取以避免阻塞UI
  • 通过直接在持久性存储区中更新对象来避免不必要的获取

本章是一个工具箱采样器;它的目的是向您展示许多抓取技术,因此当时机成熟时,您将知道使用什么工具。

NSFetchRequest:演出的星星

正如您在前面的章节中所了解的,您可以通过创建NSFetchRequest的实例来从Core Data获取记录,根据需要配置它并将其交给NSManagedObjectContext来完成繁重的工作。

看起来很简单,但实际上有五种不同的方法来获取获取请求。 其中一些比其他的更受欢迎,但是作为一个核心数据开发人员,你可能会在某个时候遇到所有这些。

在跳到本章的启动项目之前,这里有五种不同的方法来设置获取请求,这样你就不会感到惊讶了:

// 1
let fetchRequest1 = NSFetchRequest<Venue>()
let entity = 
  NSEntityDescription.entity(forEntityName: "Venue",
                             in: managedContext)!
fetchRequest1.entity = entity

// 2
let fetchRequest2 = NSFetchRequest<Venue>(entityName: "Venue")

// 3
let fetchRequest3: NSFetchRequest<Venue> = Venue.fetchRequest()

// 4
let fetchRequest4 = 
  managedObjectModel.fetchRequestTemplate(forName: "venueFR")

// 5
let fetchRequest5 =
  managedObjectModel.fetchRequestFromTemplate(
    withName: "venueFR",
    substitutionVariables: ["NAME" : "Vivi Bubble Tea"])

依次通过:

  1. NSFetchRequest的实例初始化为泛型类型:NSFetchRequest<Venue>。 至少,您必须为获取请求指定一个NSOEntityDescription。 在这种情况下,实体是Venue。 初始化NSEntityDescription的一个实例,并使用它来设置获取请求的entity属性。
  2. 这里使用NSFetchRequest的便利初始化器。 它初始化一个新的fetch请求,并在一个步骤中设置其entity属性。 您只需要为实体名称提供一个字符串,而不是完整的NSEntityDescription
  3. 就像第二个例子是第一个例子的收缩一样,第三个例子也是第二个例子的收缩。 在生成NSManagedObject子类时,此步骤也会生成一个类方法,该方法返回一个已经设置为获取相应实体类型的NSFetchRequest。 这就是Venue.fetchRequest()来自。 此代码位于`Venue+CoreDataProperties中。swift**。
  4. 在第四个示例中,您从NSManagedObjectModel检索获取请求。 您可以在Xcode的数据模型编辑器中配置和存储常用的获取请求。 你将在本章的后面学习如何做到这一点。
  5. 最后一种情况与第四种情况相似。 从托管对象模型中检索获取请求,但这一次需要传入一些额外的变量。 这些“替换”变量在谓词中使用,以优化获取的结果。

前三个示例是您已经看到的简单情况。 除了存储的获取请求和NSFetchRequest的其他技巧外,您将在本章的其余部分看到更多这些简单的例子!

Note

如果你还不熟悉它,NSFetchRequest是一个泛型类型。 如果你检查NSFetchRequest的初始化器,你会注意到它接受type作为参数〈ResultType:NSFetchRequestResult〉ResultType指定您expect的获取请求结果的对象类型。 例如,如果您期望一个Venue对象数组,则fetch请求的结果现在将是[Venue]而不是[Any]。 这是有帮助的,因为你不必再向下投到[Venue]

BubbleTea应用介绍

本章的示例项目是一个泡泡茶应用程序。对于那些不知道泡泡茶(也被称为“波巴茶”)的人来说,这是一种台湾茶基饮料,含有大的木薯珍珠。 真好吃!

你可以把这个泡泡茶应用程序看作是一个ultra-nicheYelp。 使用该应用程序,您可以找到您附近的位置出售您最喜欢的台湾饮料。

在本章中,您将只使用Foursquare中的静态场地数据:在纽约市有30个地方卖珍珠奶茶。 您将使用这些数据来构建filter/sort屏幕,以便按照您认为合适的方式排列静态场地列表。

转到本章的文件并打开`BubbleTeaFinder。**. 生成并运行启动项目。

您将看到以下内容: img

示例应用程序由许多表视图单元格组成,其中包含静态信息。 尽管示例项目目前还不是很令人兴奋,但是已经为您完成了许多设置。

打开项目导航器,查看启动项目中的完整文件列表: img

事实证明,你在本书的第一部分中必须做的大部分核心数据设置都已经准备好了,供你使用。 下面是您在初学者项目中获得的组件的快速概述,并按类别分组:

-Seed data: seed.json是一个JSON文件,其中包含纽约市提供奶茶的场所的真实世界场所数据。 由于这是来自Foursquare的真实的数据,因此结构比本书中使用的以前的种子数据更复杂。 -Data model:点击BubbleTeaFinder。xcdatamodeld打开Xcode的模型编辑器。 最重要的实体是Venue。 它包含一个地点的名称,电话号码和它目前提供的特价商品的数量的属性。 由于JSON数据相当复杂,数据模型将场地的信息分解为其他实体。 它们是CategoryLocationPriceInfoStats。 例如,Location具有城市、州、国家等属性。 -托管对象子类:您的数据模型中的所有实体都有对应的NSManagedObject子类。 它们是Venue+CoreDataClass。swift**,**Location+CoreDataClass。swift**、**PriceInfo+CoreDataClass。swift**,**Category+CoreDataClass。swiftStats+CoreDataClass。swift**。 您可以在NSManagedObject组沿着附带的中找到它们**EntityName+CoreDataProperties.swift文件。 -CoreDataStack**:和前面的章节一样,这个对象包装了一个NSPersistentContainer对象,它本身包含核心数据对象的干部,称为“stack”;上下文、模型、持久存储和持久存储协调器。 不需要设置-它是随时可用的。 -View Controller:显示场馆列表的初始视图控件为ViewController。swift。 在第一次启动时,初始从视图控制器读取seed.json**,创建相应的Core Data`对象并将其保存到持久化存储中。 点击右上角的Filter按钮,弹出FilterViewController.swift**。 现在这里没什么事。 在本章中,您将向这两个文件添加代码。

当您第一次启动示例应用程序时,您只看到静态信息。 但是,您的应用委托已从seed读取了seed.json数据,将其解析为Core Data对象并将其保存到持久化存储中。

您的第一个任务将是获取此数据并将其显示在表视图中。 这一次,你要做一个转折。

已存储的fetch请求

如前所述,您可以将经常使用的获取请求直接存储在数据模型中。 这不仅使它们更易于访问,而且还可以使用基于GUI的工具来设置获取请求参数。

打开BubbleTeaFinder。xcdatamodeld长按Add Entity按钮:

img

在菜单中选择Add Fetch Request。 这将在左侧栏上创建一个新的获取请求,并将您带到一个特殊的获取请求编辑器:

img

Note

您可以单击左侧边栏上新创建的获取请求来更改其名称。

您可以使用Xcode的数据模型编辑器中的可视化工具使您的fetch请求尽可能通用或特定。 首先,创建一个fetch请求,从持久存储中检索所有Venue对象。

你只需要在这里做一个更改:点击Fetch all旁边的下拉菜单,选择`Venue**.

img

这就是你需要做的。 如果您希望使用附加谓词来细化获取请求,也可以从获取请求编辑器添加条件。

是时候让你新创建的fetch请求运行一下了。 打开ViewController。swift并在coreDataStack下面添加以下两个属性:

var fetchRequest: NSFetchRequest<Venue>?
var venues: [Venue] = []

第一个属性将保存您的获取请求。 第二个属性是Venue对象数组,您将使用它来填充表格视图。

接下来,在viewDidLoad()的末尾添加以下内容:

guard let model = 
  coreDataStack.managedContext
    .persistentStoreCoordinator?.managedObjectModel,
  let fetchRequest = model
    .fetchRequestTemplate(forName: "FetchRequest")
    as? NSFetchRequest<Venue> else {
      return
}

self.fetchRequest = fetchRequest
fetchAndReload()

这样做会将刚刚设置的fetchRequest属性连接到使用Xcode的数据模型编辑器创建的属性。 这里有三件事要记住:

  1. 与其他获取获取请求的方法不同,这种方法涉及托管对象模型。 这就是为什么你必须通过coreDataStack属性来检索你的fetch请求。
  2. 正如您在上一章中看到的,您构造了CoreDataStack,因此只有托管上下文是公共的。 要检索托管对象模型,必须通过托管上下文的持久存储协调器。
  3. NSManagedObjectModelfetchRequestTemplate(forName:)采用字符串标识符。 此标识符必须与您在模型编辑器中为获取请求选择的名称完全匹配。 否则,您的应用将抛出异常并崩溃。

最后一行调用了一个你还没有定义的方法,所以Xcode会抱怨它。要解决此问题,请在UITableViewDataSource扩展上方添加以下扩展:

// MARK: - Helper methods
extension ViewController {

  func fetchAndReload() {

    guard let fetchRequest = fetchRequest else {
      return
    }

    do {
      venues =
        try coreDataStack.managedContext.fetch(fetchRequest)
      tableView.reloadData()
    } catch let error as NSError {
      print("Could not fetch \(error), \(error.userInfo)")
    }
  }
}

顾名思义,fetchAndReload()执行获取请求并重新加载表视图。 这个类中的其他方法将需要查看获取的对象,因此您将获取的结果存储在前面定义的venues属性中。

在运行示例项目之前,您还必须做一件事:用获取的Venue对象连接表视图的数据源。

UITableViewDataSource扩展中,将tableView(_:numberOfRowsInSection:)tableView(_:cellForRowAt:)的占位符实现替换为以下内容:

func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
  venues.count
}

func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath)
               -> UITableViewCell {

  let cell =
    tableView.dequeueReusableCell(
      withIdentifier: venueCellIdentifier, for: indexPath)

  let venue = venues[indexPath.row]
  cell.textLabel?.text = venue.name
  cell.detailTextLabel?.text = venue.priceInfo?.priceCategory  
  return cell
}

在本书中,您已经多次实现了这些方法,因此您可能对它们的功能很熟悉。 第一个方法,tableView(_:numberOfRowsInSection:),将表格视图中单元格的数量与venues数组中获取的对象的数量进行匹配。

第二个方法,tableView(_:cellForRowAt:),将给定索引路径的单元格出队,并使用venue数组中相应的Venue的信息填充它。 在本例中,主标签获取地点的名称,详细信息标签获取价格类别,该价格类别是三个可能值之一:$$$$$

构建并运行项目,您将看到以下内容:

img

向下滚动泡泡茶场所列表。 这些都是纽约市真正的地方,出售美味的饮料。

Note

什么时候应该在数据模型中存储fetch请求? 如果您知道您将在应用的不同部分反复执行相同的获取操作,您可以使用此功能来保存多次编写相同的代码。 存储获取请求的缺点是无法指定结果的排序顺序。 因此,你看到的场地列表可能与书中的顺序不同。

获取不同的结果类型

一直以来,您可能一直认为NSFetchRequest是一个相当简单的工具。 你给予它一些指令,你得到一些对象作为回报。 还能怎么样

如果是这样的话,你就低估了这个班级。 NSFetchRequestCore Data框架的多功能瑞士军刀!

您可以使用它来获取单个值,计算数据的统计数据,例如平均值,最小值,最大值等。

你会问,这怎么可能? NSFetchRequest有一个名为resultType的属性。 到目前为止,您只使用了默认值。managedObjectResultType。 以下是fetch请求的resultType的所有可能值:

-.managedObjectResultType:返回托管对象(默认值)。 -.countResultType`**:返回与提取请求匹配的对象的计数。 -.dictionaryResultType**:这是一种用于返回不同计算结果的全面返回类型。 -``.managedObjectIDResultType**:返回唯一标识符,而不是完整的托管对象。

让我们回到示例项目并在实践中应用这些概念。

示例项目运行后,点击右上角【过滤】,弹出过滤界面。

您现在不会实现实际的过滤器或排序。 相反,您将关注以下四个标签:

让我们回到示例项目并在实践中应用这些概念。

示例项目运行后,点击右上角Filter,弹出过滤界面。

您现在不会实现实际的过滤器或排序。 相反,您将关注以下四个标签:

img

过滤网分为三段:PriceMost PopularSort By。 最后一部分在技术上并不是由“过滤器”组成的,但是排序通常与过滤器密切相关,所以就这样吧。

每个价格过滤器下面是属于该价格类别的场馆总数的空间。 同样,有一个位置的交易总数在所有场所。 接下来您将实现这些。

返回计数

打开FilterViewController。swift并在import UIKit下面添加如下内容:

import CoreData

接下来,在最后一个@IBOutlet属性下面添加以下属性:

// MARK: - Properties
var coreDataStack: CoreDataStack!

这将保存对您在ViewController中使用的CoreDataStack`对象的引用。swift**。

接下来打开ViewController。swift并将prepare(for:sender:)实现替换为以下内容:

override func prepare(for segue: UIStoryboardSegue,
                      sender: Any?) {

  guard segue.identifier == filterViewControllerSegueIdentifier,
    let navController = segue.destination
      as? UINavigationController,
    let filterVC = navController.topViewController
      as? FilterViewController else {
        return
  }

  filterVC.coreDataStack = coreDataStack
}

新代码行将CoreDataStack对象从ViewController传播到FilterViewController。 过滤器屏幕现在准备好使用核心数据。

打开FilterViewController。swift并在coreDataStack下面添加如下lazy属性:

lazy var cheapVenuePredicate: NSPredicate = {
  return NSPredicate(format: "%K == %@", 
    #keyPath(Venue.priceInfo.priceCategory), "$")
}()

您将使用这个惰性实例化的NSPerdicate来计算最低价格类别中的场地数量。

Note

NSPerdicate支持基于字符串的密钥路径。 这就是为什么您可以使用priceInfoVenue实体向下钻取到PriceInfo实体的原因。priceCategory,并使用#keyPath关键字来获取键路径的安全的、编译时检查过的值。 在撰写本文时,NSPerdicate不支持Swift 4风格的键路径,例如\Venue。priceInfo.priceCategory

接下来,在UITableViewDelegate扩展下面添加以下扩展:

// MARK: - Helper methods
extension FilterViewController {

  func populateCheapVenueCountLabel() {

    let fetchRequest =
      NSFetchRequest<NSNumber>(entityName: "Venue")
    fetchRequest.resultType = .countResultType
    fetchRequest.predicate = cheapVenuePredicate

    do {
      let countResult =
        try coreDataStack.managedContext.fetch(fetchRequest)

      let count = countResult.first?.intValue ?? 0
      let pluralized = count == 1 ? "place" : "places"
      firstPriceCategoryLabel.text = 
        "\(count) bubble tea \(pluralized)"
    } catch let error as NSError {
      print("count not fetched \(error), \(error.userInfo)")
    }
  }
}

这个扩展提供了populateCheapVenueCountLabel(),它创建了一个获取请求来获取Venue实体。 然后将结果类型设置为。countResultType并将提取请求的谓词设置为cheapVenuePredicate。 请注意,要使其正常工作,获取请求的类型参数必须是NSNumber,而不是Venue

当您将获取结果的结果类型设置为时。countResultType,返回值为Swift数组,包含单个NSNumberNSNumber中的整数是您要查找的总数。

再次,您将针对CoreDataStackNSManagedObjectContext属性执行fetch请求。 然后从结果NSNumber中提取整数,并使用它填充firstPriceCategoryLabel

在运行示例应用之前,将以下内容添加到viewDidLoad()的底部:

populateCheapVenueCountLabel()

现在构建并运行以测试这些更改是否生效。 点击Filter,弹出过滤/排序菜单:

img

第一个价格过滤器下面的标签现在写着“27个泡泡茶地点。“ 万岁! 您已成功使用NSFetchRequest计算计数。

Note

您可能会想,您可以同样轻松地获取实际的Venue对象,并从数组的count属性中获取计数。 那倒是 获取计数而不是对象主要是一种性能优化。 例如,如果您有纽约市的人口普查数据,并想知道有多少人居住在其大都市区,您更愿意Core Data给您提供数字8,300,000(整数)还是8,300,000条记录的数组? 显然,直接获取计数更节省内存。 有整整一章专门介绍核心数据性能。 如果您想了解更多有关Core Data中的性能优化的信息,请查看第8章“测量和提升性能”。“

现在您已经熟悉了计数结果类型,可以快速实现第二个价格类别过滤器的计数。 在cheapVenuePredicate下面添加如下lazy属性:

lazy var moderateVenuePredicate: NSPredicate = {
  return NSPredicate(format: "%K == %@", 
    #keyPath(Venue.priceInfo.priceCategory), "$$")
}()

这个NSPerdicatecheap venue谓词几乎相同,除了这个谓词匹配$$而不是$**。 同样,在populateCheapVenueCountLabel()`下面添加如下方法:

func populateModerateVenueCountLabel() {

  let fetchRequest = 
    NSFetchRequest<NSNumber>(entityName: "Venue")
  fetchRequest.resultType = .countResultType
  fetchRequest.predicate = moderateVenuePredicate

  do {

    let countResult = 
      try coreDataStack.managedContext.fetch(fetchRequest)

    let count = countResult.first?.intValue ?? 0
    let pluralized = count == 1 ? "place" : "places"
    secondPriceCategoryLabel.text = 
      "\(count) bubble tea \(pluralized)"
  } catch let error as NSError {
    print("count not fetched \(error), \(error.userInfo)")
  }
}

最后,在viewDidLoad()的底部添加以下行,以调用新定义的方法:

populateModerateVenueCountLabel()

生成并运行示例项目。 如前所述,点击右上方的Filter,进入filter/sort界面:

img

对奶茶爱好者的好消息! 只有两个地方比较贵。 泡泡茶作为一个整体似乎是相当容易接近的。

获取计数的另一种方法

既然你已经熟悉了。countResultType,这是一个很好的时机来提一下,有一个替代的API直接从核心数据获取计数。

由于还有一个价格类别计数要实现,现在您将使用这个备用API

moderateVenuePredicate下面添加如下lazy属性:

lazy var expensiveVenuePredicate: NSPredicate = {
  return NSPredicate(format: "%K == %@", 
    #keyPath(Venue.priceInfo.priceCategory), "$$$")
}()

接下来,在populateModerateVenueCountLabel()下面实现如下方法:

func populateExpensiveVenueCountLabel() {

  let fetchRequest: NSFetchRequest<Venue> = Venue.fetchRequest()
  fetchRequest.predicate = expensiveVenuePredicate

  do {
    let count =
      try coreDataStack.managedContext.count(for: fetchRequest)
    let pluralized = count == 1 ? "place" : "places"
    thirdPriceCategoryLabel.text = 
      "\(count) bubble tea \(pluralized)"
  } catch let error as NSError {
    print("count not fetched \(error), \(error.userInfo)")
  }
}

与前两个场景一样,您创建了一个获取请求来检索Venue对象。

接下来,设置前面定义为惰性属性的谓词:expensiveVenuePredicate

这个场景与前两个场景的区别在于,这里没有将结果类型设置为。countResultType。 使用NSManagedObjectContext的方法count(for:),而不是通常的fetch(_:)

count(for:)的返回值是一个整数,您可以直接使用它来填充第三个价格类别标签。 最后,在viewDidLoad()的底部添加以下行,以调用新定义的方法:

populateExpensiveVenueCountLabel()

生成并运行以查看最新更改是否生效。

filter/sort屏幕应如下所示:

img

只有一个泡泡茶场所福尔斯$$类别。 也许他们用真实的珍珠代替木薯粉?

使用fetch请求进行计算

所有三个价格类别标签都填充有属于每个类别的场地数量。 下一步是填充Offering a deal下的标签。它目前说0 total deals这不可能!

这些信息究竟从何而来? Venue有一个specialCount属性,用于捕获该地点当前提供的交易数量。 与价格类别下的标签不同,您现在需要知道all场所的交易总额,因为一个特别精明的场所可能一次有许多交易。

最简单的方法是将所有场地加载到内存中,并使用for循环对它们的交易求和。 如果你希望有一个更好的方法,你很幸运:Core Data内置支持多种不同的函数,例如平均值、总和、最小值和最大值。

打开FilterViewController。swift**,并在populateExpensiveVenueCountLabel()`下面添加如下方法:

func populateDealsCountLabel() {

  // 1
  let fetchRequest = 
    NSFetchRequest<NSDictionary>(entityName: "Venue")
  fetchRequest.resultType = .dictionaryResultType

  // 2
  let sumExpressionDesc = NSExpressionDescription()
  sumExpressionDesc.name = "sumDeals"

  // 3
  let specialCountExp = 
    NSExpression(forKeyPath: #keyPath(Venue.specialCount))
  sumExpressionDesc.expression = 
    NSExpression(forFunction: "sum:",
                 arguments: [specialCountExp])
  sumExpressionDesc.expressionResultType =
    .integer32AttributeType

  // 4
  fetchRequest.propertiesToFetch = [sumExpressionDesc]

  // 5
  do {

    let results = 
      try coreDataStack.managedContext.fetch(fetchRequest)

    let resultDict = results.first
    let numDeals = resultDict?["sumDeals"] as? Int ?? 0
    let pluralized = numDeals == 1 ?  "deal" : "deals"
    numDealsLabel.text = "\(numDeals) \(pluralized)"

  } catch let error as NSError {
    print("count not fetched \(error), \(error.userInfo)")
  }
}

这个方法包含了一些你以前在书中没有遇到过的类,所以这里依次解释了每个类:

  1. 开始,创建用于检索Venue对象的典型fetch请求。 接下来,将结果类型指定为。dictionaryResultType
  2. 您创建一个NSExpressionDescription来请求求和,并将其命名为sumDeals,这样您就可以从fetch请求返回的结果字典中读取其结果。
  3. 您为表达式描述指定一个NSPryption,以指定您想要的sum函数。 接下来,给予that表达式另一个NSExpression来指定要求和的属性-在本例中是specialCount。 最后,您必须设置表达式描述的返回数据类型,因此将其设置为integer 32AttributeType
  4. 您可以通过将其propertiesToFetch属性设置为您刚刚创建的表达式描述来告诉您的原始获取请求获取sum
  5. 最后,在通常的do-catch语句中执行fetch请求。 结果类型是一个NSDictionary数组,因此您可以使用表达式描述的名称(sumDeals)检索表达式的结果,这样就完成了!

Note

Core Data还支持哪些功能? 举几个例子:countminmaxaveragemedianmodeabsolute value等等。 有关完整列表,请查看AppleNSExpression文档。

Core Data获取计算值需要执行许多通常不直观的步骤,因此请确保您有充分的理由使用此技术,例如性能考虑。 最后,在viewDidLoad()的底部添加以下行:

populateDealsCountLabel()

构建示例项目并打开filter/sort屏幕以验证更改。

img

太好了! 在Core Data中存储了所有venues的12个交易。

您现在已经使用了四种支持的NSFetchRequest结果类型中的三种:.managedObjectResultType.countResultType。dictionaryResultType

剩下的结果类型是。managedObjectIDResultType。 当您使用此类型获取时,结果是NSManagedObjectID对象的数组,而不是它们所代表的实际托管对象。 NSManagedObjectID是托管对象的紧凑通用标识符。 它的工作原理就像数据库中的主键!

iOS 5之前,按ID获取很流行,因为NSManagedObjectID是线程安全的,使用它可以帮助开发人员实现线程限制并发模型。

现在线程限制已经被弃用,而更现代的并发模型更受欢迎了,所以没有什么理由再按对象ID获取。

Note

您可以设置多个托管对象上下文来运行并发操作,并将长时间运行的操作保持在主线程之外。 有关详细信息,请参阅第9章“多个托管对象上下文”。“

您已经体验了获取请求可以为您做的所有事情。 但是,与获取请求返回的信息同样重要的是它doesn’t返回的信息。 出于实际原因,您必须在某个时候对传入的数据进行封顶。

为什么? 想象一个完美连接的对象图,其中每个Core Data对象通过一系列关系连接到其他对象。 如果Core Data没有限制获取请求返回的信息,那么每次都要获取整个对象图! 这不是内存效率。

您可以手动限制从获取请求中获取的信息。 例如,NSFetchRequest支持批量取数。 您可以使用属性fetchBatchSizefetchLimitfetchOffset来控制批处理行为。

Core Data还尝试通过使用一种称为faulting的技术来最大限度地减少内存消耗。 fault是一个占位符对象,表示尚未完全进入内存的托管对象。

另一种限制对象图的方法是使用谓词,就像上面填充地点计数标签所做的那样。 让我们使用谓词将过滤器添加到示例应用程序中。

打开`FilterViewController。swift**,并在类定义上方添加以下协议声明:

protocol FilterViewControllerDelegate: class {
  func filterViewController(
    filter: FilterViewController,
    didSelectPredicate predicate: NSPredicate?,
    sortDescriptor: NSSortDescriptor?)
}

此协议定义了一个委托方法,当用户选择新的sort/filter组合时,该方法将通知委托。

接下来,在coreDataStack下面添加以下三个属性:

weak var delegate: FilterViewControllerDelegate?
var selectedSortDescriptor: NSSortDescriptor?
var selectedPredicate: NSPredicate?

第一个属性将保存对FilterViewController的委托的引用。 它是一个weak属性,而不是一个强保留属性,以避免保留循环。 第二个和第三个属性将分别保存对当前选定的NSSortDescriptorNSPerdicate的引用。

接下来实现search(_:),如下所示:

@IBAction func search(_ sender: UIBarButtonItem) {
  delegate?.filterViewController(
    filter: self,
    didSelectPredicate: selectedPredicate,
    sortDescriptor: selectedSortDescriptor)

  dismiss(animated: true)
}

这意味着每次点击filter/sort屏幕右上角的Search 时,您将通知代表您的选择,并关闭filter/sort屏幕以显示其后面的场地列表。

您需要在此文件中再做一次更改。 找到tableView(_:didSelectRowAt:),实现如下:

override func tableView(_ tableView: UITableView,
                        didSelectRowAt indexPath: IndexPath) {

  guard let cell = tableView.cellForRow(at: indexPath) else {
    return
  }

  // Price section
  switch cell {
  case cheapVenueCell:
    selectedPredicate = cheapVenuePredicate
  case moderateVenueCell:
    selectedPredicate = moderateVenuePredicate
  case expensiveVenueCell:
    selectedPredicate = expensiveVenuePredicate
  default: break
  }

  cell.accessoryType = .checkmark
}

当用户点击前三个价格类别单元格中的任何一个时,该方法将所选单元格映射到适当的谓词。 在selectedPredicate中存储对该谓词的引用,以便在通知委托用户的选择时准备就绪。

接下来打开ViewController。swift并添加如下扩展,以符合FilterViewControllerDelegate协议:

// MARK: - FilterViewControllerDelegate
extension ViewController: FilterViewControllerDelegate {

  func filterViewController(
    filter: FilterViewController,
    didSelectPredicate predicate: NSPredicate?,
    sortDescriptor: NSSortDescriptor?) {

    guard let fetchRequest = fetchRequest else {
      return
    }

    fetchRequest.predicate = nil
    fetchRequest.sortDescriptors = nil

    fetchRequest.predicate = predicate

    if let sort = sortDescriptor {
      fetchRequest.sortDescriptors = [sort]
    }

    fetchAndReload()
  }
}

添加FilterViewControllerDelegate Swift扩展将告诉编译器该类将符合此协议。 每当用户选择新的filter/sort组合时,此委托方法都会触发。

在这里,您重置了fetch请求的predicatesortDescriptors,然后设置传递到方法中的谓词和排序描述符,并重新加载数据。

在测试价格类别过滤器之前,您还需要做一件事。 找到prepare(for:sender:)并在方法末尾添加以下行:

filterVC.delegate = self

这将ViewController正式设置为FilterViewController的委托。

生成并运行示例项目。 进入Filter面,点击第一个价格类别单元格($),点击右上角的Search

您的应用崩溃,并在控制台中显示以下错误消息:

2020-09-20 11:47:40.872640-0400 BubbleTeaFinder[65767:8506463]`* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Can't modify a named fetch request in an immutable model.'

发生了什么事? 在本章的前面,您在数据模型中定义了获取请求。 事实证明,如果使用这种技术,fetch请求将变得不可变。 你不能在运行时更改它的谓词,否则你会崩溃。 如果您想以任何方式修改fetch请求,您必须提前在数据模型编辑器中进行修改。

打开ViewController。swift**,将viewDidLoad()`替换为:

override func viewDidLoad() {
  super.viewDidLoad()

  importJSONSeedDataIfNeeded()

  fetchRequest = Venue.fetchRequest()
  fetchAndReload()
}

您删除了从托管对象模型中的模板检索提取请求的行。 相反,您直接从Venue实体获得NSFetchRequest的实例。

再次构建并运行示例应用程序。 进入【筛选】页面,点击第二个价格类别单元格($$),点击右上角的【搜索】。

这就是结果:

img

正如预期的那样,这个类别中只有两个场地。 同时测试第一个($)和第三个($$)价格类别过滤器,确保过滤后的列表包含每个类别的正确场地数量。

您将练习为其余的过滤器编写更多的谓词。 这个过程和你已经做过的类似,所以这次你会做更少的解释。

打开FilterViewController。swift并在expensiveVenuePredicate下面添加这三个惰性属性:

lazy var offeringDealPredicate: NSPredicate = {
  return NSPredicate(format: "%K > 0",
    #keyPath(Venue.specialCount))
}()

lazy var walkingDistancePredicate: NSPredicate = {
  return NSPredicate(format: "%K < 500",
    #keyPath(Venue.location.distance))
}()

lazy var hasUserTipsPredicate: NSPredicate = {
  return NSPredicate(format: "%K > 0",
    #keyPath(Venue.stats.tipCount))
}()

第一个谓词指定当前提供一个或多个交易的场所,第二个谓词指定距离您当前位置小于500米的场所,第三个谓词指定至少有一个用户提示的场所。

Note

到目前为止,您已经编写了带有单个条件的谓词。 您还应该知道,您可以通过使用复合谓词运算符(如AND**、**ORNOT**)来编写谓词来检查两个条件而不是一个条件。 或者,您可以使用类NSCompoundPredicate将两个简单谓词串成一个复合谓词。NSPerdicate在技术上不是核心数据的一部分(它是Foundation的一部分),所以本书不会深入介绍它,但是你可以通过学习这个漂亮的类的来龙去脉来认真提高你的核心数据。 有关更多信息,请务必查看ApplePredicate`编程指南: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/Articles/pUsing.html

接下来,向下滚动到tableView(_:didSelectRowAt:)。 您将在前面添加的switch语句中添加另外三个case:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

  guard let cell = tableView.cellForRow(at: indexPath) else {
    return
  }

  switch cell {
  // Price section
  case cheapVenueCell:
    selectedPredicate = cheapVenuePredicate
  case moderateVenueCell:
    selectedPredicate = moderateVenuePredicate
  case expensiveVenueCell:
    selectedPredicate = expensiveVenuePredicate

  // Most Popular section
  case offeringDealCell:
    selectedPredicate = offeringDealPredicate
  case walkingDistanceCell:
    selectedPredicate = walkingDistancePredicate
  case userTipsCell:
    selectedPredicate = hasUserTipsPredicate
  default: break
  }

  cell.accessoryType = .checkmark
}

在上面,您添加了offeringDealCellwalkingDistanceCelluserTipsCell的案例。 这是您现在要添加支持的三个新过滤器。

这就是你需要做的。 生成并运行示例应用程序。进入Filters页面,选择Offering a deal筛选,点击Search:

img

你会看到总共六个场馆。 请注意,由于您没有指定排序描述符,因此您的场地列表的顺序可能与屏幕截图中的场地不同。 您可以通过在seed.json**中查找来验证这些场所是否有特价。 例如,City Wing Cafe`目前提供四种特价。 喔-呼!

对提取结果进行排序

NSFetchRequest的另一个强大功能是它能够为您对获取的结果进行排序。 它通过使用另一个方便的FoundationNSSortDescriptor来实现这一点。 这些排序发生在SQLite级别,而不是在内存中。 这使得在核心数据中的排序快速而有效。

在本节中,您将实现四种不同的排序来完成筛选/排序屏幕。

打开FilterViewController。swift并在hasUserTipsPredicate下面添加以下三个惰性属性:

lazy var nameSortDescriptor: NSSortDescriptor = {
  let compareSelector =
    #selector(NSString.localizedStandardCompare(_:))
  return NSSortDescriptor(key: #keyPath(Venue.name),
                          ascending: true,
                          selector: compareSelector)
}()

lazy var distanceSortDescriptor: NSSortDescriptor = {
  return NSSortDescriptor(
    key: #keyPath(Venue.location.distance),
    ascending: true)
}()

lazy var priceSortDescriptor: NSSortDescriptor = {
  return NSSortDescriptor(
    key: #keyPath(Venue.priceInfo.priceCategory),
    ascending: true)
}()

添加排序描述符的方法与添加筛选器的方法非常相似。 每个排序描述符映射到这三个惰性NSSortDescriptor属性之一。

要初始化NSSortDescriptor的实例,您需要三件事:指定要排序的属性的键路径,指定排序是升序还是降序的规范,以及执行比较操作的可选选择器。

Note

如果你以前使用过NSSortDescriptor,那么你可能知道有一个基于块的API,它采用比较器而不是选择器。 不幸的是,Core Data不支持这种定义排序描述符的方法。 同样的事情也适用于定义NSPerdicate的基于块的方法。 核心数据也不支持这一点。 原因是过滤和排序发生在SQLite数据库中,因此谓词/排序描述符必须与可以写成SQL语句的内容很好地匹配。

这三个排序描述符将分别按名称、距离和价格类别以升序排序。 在继续之前,仔细看看第一个排序描述符nameSortDescriptor。 初始化器接受一个可选的选择器NSString。localizedStandardCompare(_:). 那是什么?

任何时候你对面向用户的字符串进行排序时,Apple建议你传入NSString。localizedStandardCompare(_:)根据当前区域设置的语言规则进行排序。 这意味着排序将“正常工作”,并为具有特殊字符的语言做正确的事情。 这是小事情的重要性,bien sûr

接下来,找到tableView(_:didSelectRowAt:),并在default case上面的switch语句末尾添加以下case:

// Sort By section
case nameAZSortCell:
  selectedSortDescriptor = nameSortDescriptor
case nameZASortCell:
  selectedSortDescriptor =
    nameSortDescriptor.reversedSortDescriptor
    as? NSSortDescriptor
case distanceSortCell:
  selectedSortDescriptor = distanceSortDescriptor
case priceSortCell:
  selectedSortDescriptor = priceSortDescriptor

像前面一样,这个switch语句将用户点击的单元格与适当的排序描述符相匹配,因此当用户点击Search时,它就可以传递给委托。

唯一的问题是nameZA排序描述符。 您可以为A-Z重用该描述符,而不是创建单独的排序描述符,并简单地调用方法reversedSortDescriptor。 多方便啊!

其他所有的东西都连接起来,以便测试刚刚实现的排序。 构建并运行示例应用,进入Filter屏幕。 点击Name (Z-A)排序,然后点击`Search**。 你会看到搜索结果是这样排序的:

img

不,你没有看到重影。 数据集中确实有七个Vivi Bubble Tea场所-这是纽约市一家受欢迎的泡泡茶连锁店。

当你向下滚动表格视图时,你会看到应用程序确实按字母顺序从Z到A对场地进行了排序。

您现在已经完成了Filter屏幕的设置,用户可以将任何一个过滤器与任何一种排序组合在一起。 尝试不同的组合,看看你得到什么。 venue单元格没有显示太多信息,所以如果你需要验证一个排序,你可以直接去源代码,咨询`seed.json**。

异步取数

如果你已经到了这一步,就会有好消息和坏消息(然后是更多的好消息)。 好消息是,您已经了解了很多关于如何使用普通的NSFetchRequest的信息。 坏消息是,到目前为止,您执行的每个fetch请求都阻塞了主线程,而您正在等待结果返回。

当你阻塞主线程时,它会使屏幕对输入的触摸没有反应,并产生一系列其他问题。 您没有感觉到主线程的阻塞,因为您已经发出了简单的fetch请求,每次获取几个对象。

自从Core Data开始以来,该框架为开发人员提供了几种在后台执行提取的技术。 从iOS 8开始,Core Data有一个API,用于在后台执行长时间运行的获取请求,并在获取完成时获取完成回调。

让我们看看这个新的API在实际操作中。 打开ViewController。swift并在venue下面添加以下属性:

var asyncFetchRequest: NSAsynchronousFetchRequest<Venue>?

这就对了负责这个异步魔法的类被恰当地称为NSAsynchronousFetchRequest。 不要被它的名字所迷惑。 与NSFetchRequest没有直接关系; 它实际上是NSPersistentStoreRequest的子类。

接下来,将viewDidLoad()的内容替换为以下内容:

override func viewDidLoad() {
  super.viewDidLoad()

  importJSONSeedDataIfNeeded()

  // 1
  let venueFetchRequest: NSFetchRequest<Venue> = 
    Venue.fetchRequest()
  fetchRequest = venueFetchRequest

  // 2
  asyncFetchRequest =
    NSAsynchronousFetchRequest<Venue>(
    fetchRequest: venueFetchRequest) {
      [unowned self] (result: NSAsynchronousFetchResult) in

      guard let venues = result.finalResult else {
        return
      }

      self.venues = venues
      self.tableView.reloadData()
  }

  // 3
  do {
    guard let asyncFetchRequest = asyncFetchRequest else {
      return
    }
    try coreDataStack.managedContext.execute(asyncFetchRequest)
    // Returns immediately, cancel here if you want
  } catch let error as NSError {
    print("Could not fetch \(error), \(error.userInfo)")
  }
}

有很多你以前没有见过的,所以让我们一步一步地覆盖它:

  1. 请注意,异步获取请求并没有取代常规获取请求。 相反,您可以将异步获取请求看作是围绕已经拥有的获取请求的wrapper
  2. 要创建NSAsynchronousFetchRequest,需要两件事:一个普通的旧NSFetchRequest和一个完成处理程序。 您获取的场馆包含在NSAsynchronousFetchResultfinalResult属性中。 在完成处理程序中,更新venues属性并重新加载表视图。
  3. 指定完成处理程序是不够的! 您仍然必须执行异步获取请求。 同样,CoreDataStackmanagedContext属性为您处理繁重的工作。 但是,请注意,您使用的方法不同-这一次,它是execute(_:)而不是通常的fetch(_:)

execute(_:)立即返回。 您不需要对返回值做任何事情,因为您将从完成块中更新表视图。 返回类型为NSAsynchronousFetchResult

Note

作为该API的额外好处,您可以使用NSAsynchronousFetchResultcancel()方法取消获取请求。

是时候看看异步获取是否按承诺交付了。 如果一切顺利,您应该不会注意到用户界面的任何差异。

构建并运行示例应用程序,您应该会看到与之前一样的场地列表:

img

万岁! 您已经掌握了异步获取。 过滤器和排序也可以工作,只是它们仍然使用普通的NSFetchRequest来重新加载表视图。

批量更新:不需要提取

有时候,从Core Data获取对象的唯一原因是更改单个属性。 然后,在您做出更改之后,您必须将Core Data对象提交回持久性存储,并就此结束。 这是你沿着遵循的正常程序。

但是,如果您想一次更新十万条记录,该怎么办? 仅仅为了更新一个属性,获取所有这些对象将花费大量的时间和大量的内存。 再多的调整你的获取请求也不能保存你的用户不必长时间地盯着一个微调器。

幸运的是,从iOS 8开始,有了新的方法来更新Core Data对象,而无需将任何内容提取到内存中:batch updates。 这种新技术大大减少了进行这些大型更新所需的时间和内存。

新技术绕过了NSManagedObjectContext,直接进入持久存储区。 批量更新的经典用例是消息传递应用程序或电子邮件客户端中的“全部标记为已读”特性。 对于这个示例应用程序,您将做一些更有趣的事情。 既然你这么喜欢泡泡茶,你就要把核心数据中的每一个“地点”都标记为你的最爱。

让我们在实践中看看。 打开ViewController。swift并在importJSONSeedDataIfNeeded()调用下面的viewDidLoad()中添加以下内容:

let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")
batchUpdate.propertiesToUpdate = 
  [#keyPath(Venue.favorite): true]

batchUpdate.affectedStores = 
  coreDataStack.managedContext
    .persistentStoreCoordinator?.persistentStores

batchUpdate.resultType = .updatedObjectsCountResultType

do {
  let batchResult = 
    try coreDataStack.managedContext.execute(batchUpdate)
      as? NSBatchUpdateResult
  print("Records updated \(String(describing: batchResult?.result))")
} catch let error as NSError {
  print("Could not update \(error), \(error.userInfo)")
}

您使用要更新的实体创建了一个NSBatchUpdateRequest的实例,在本例中为Venue

接下来,通过将propertiesToUpdate设置为包含要更新的属性favorite的键路径及其新值true的字典,设置批更新请求。 然后将affectedStores设置为持久存储协调器的persistentStores数组。

最后,您需要输入返回计数的结果类型并执行批处理更新请求.

构建并运行示例应用程序。如果一切正常,您将看到以下内容打印到控制台日志中:

Records updated 30

太好了! 你偷偷地把纽约市的每一家奶茶店标为你最爱的地方。

现在,您知道了如何在不将核心数据对象加载到内存中的情况下更新它们。 是否有其他用例,您可能希望绕过托管上下文并直接在持久性存储中更改Core Data对象?

当然有-批量删除!

你不应该仅仅为了删除对象而将它们加载到内存中,特别是当你处理大量对象的时候。 从iOS 9开始,你已经有了NSBatchDeleteRequest用于此目的。

顾名思义,批量删除请求可以高效地一次性删除大量Core Data对象。

NSBatchUpdateRequest一样,NSBatchDeleteRequest也是NSPersistentStoreRequest的子类。 这两种类型的批处理请求的行为类似,因为它们都直接在持久性存储上操作。

Note

由于您避开了NSManagedObjectContext,因此如果您使用批处理更新请求或批处理删除请求,您将不会获得任何验证。 您的更改也不会反映在托管上下文中。 在使用持久性存储请求之前,请确保您正确地清理和验证了数据!

要点

  • NSFetchRequestgeneric type。 它接受一个类型参数,该参数指定您期望作为获取请求的结果获得的对象的类型。
  • 如果您希望在应用的不同部分重复使用相同类型的fetch,可以考虑使用Data Model Editor 直接在数据模型中存储immutable 的fetch请求。
  • 使用NSFetchRequestcount结果类型有效地计算并从SQLite返回计数。
  • 使用NSFetchRequestdictionaryresult类型有效地计算并返回SQLite中的平均值、总和和其他常见计算。
  • 获取请求使用不同的技术,例如使用batch sizesbatch limitsfaulting来限制返回的信息量。
  • 在获取请求中添加sort description,以高效地对获取的结果进行排序。
  • 获取大量信息可能会阻塞主线程。 使用NSAsynchronousFetchRequest将部分工作卸载到后台线程。
  • NSBatchUpdateRequestNSBatchDeleteRequest减少更新或删除Core Data中大量记录所需的时间和内存。