KSPFetchedResultsController 1.1.1

KSPFetchedResultsController 1.1.1

测试已测试
语言语言 Obj-CObjective C
许可协议 MIT
发布日期最后发布日期2015 年 10 月

Konstantin Pavlikhin 维护。



专为桌面 Cocoa 重新实施的最高级的 NSFetchedResultsController

假设您遇到一个问题并希望使用 CoreData。恭喜,现在您有两个问题😂.

原理

CoreData 于 10.4 Tiger(2005 年)推出后,多年来开发人员一直在使用这两个中介控制器之一构建 AppKit 应用程序。

NSArrayController

确实可以使用 NSArrayController 与 CoreData(如果您的交互性需求相对简单)。

NSTreeController

NSTreeController 完全无效,应该被活活烧死。

我对 NSTreeController 的个人抱怨如下

  • 使用不透明、未记录的阴影对象包装实际传入的对象
  • 没有细粒度的 NSOutlineView 更改管理
  • 没有为项目筛选绑定 NSPredicate 的选项
  • 在大型数据集上的性能极差

我多次尝试使用这种毫无意义的东西,都失败了。

在 OS X 10.7 狮子中,NSTableView/NSOutlineView 类经过了彻底的重构。它们开始允许基于 NSView 的单元格,而不是丑陋且极不方便的基于 NSCell 的单元格。第二大重要变化是能够对表示的数据集进行增量更改(插入行/移动行/删除行/更新行)并动画相应的转换。所有这些更改使 NSTableView/NSOutlineView 对的行为和外观更像 UIKit 中的 UITableView。不幸的是,现有的控制器对象(NSArrayControllerNSTreeController)并未更新以利用最新的增强功能。如果模型发生变化,NSObjectController 后代将简单通过 -reloadData 方法重新加载整个表。对我来说,这感觉像一个无礼、冗余且不优雅的解决方案,可能具有较差的性能(如果您有一个具有可变行高的表,则表将丢失缓存并重新查询每行的高度,即使是不可见的行)。使用新表格视图功能的方法是丢弃这些粘合对象并以手动方式处理表格。

由于某种原因,苹果决定不将 NSFetchedResultsController 移植到 OS X 上。鉴于苹果一直以来的坏习惯,即使是再荒谬的bug也不会修复,所以这个缺乏获取结果控制器的微小问题并不让我感到惊讶。

让我们为自己设计一个获取结果控制器

原始的 NSFetchedResultsController 是为了在 iOS 上运行并嵌入到 UITableView 实例中设计的。鉴于 UITableView 的 API 与 AppKit 的 NSTableView 的差异,我们不能简单地复制粘贴类接口并编写 FRC 实现。无法创建一个有用的即插即用组件。相反,我们必须移植基本思想,并将其集成到现有的 AppKit 基础设施中。

NSTableViewUITableView 之间最显著的不同之处在于后者有一个表分区概念。UITableView 会“请求”其代理返回特定索引路径上的对象。而 NSTableView 最多只能显示所谓的分组行,但底层模型仍然必须是平的(NSArray)。幸运的是,我们可以使用支持任意项嵌套的 NSOutlineView 来模拟分区。NSOutlineView 不会操作索引路径,而是使用一个概念为项,这些项可以成为其他项的子项。这意味着我们需要一种特殊类型的项来表示大纲视图中的分区。这就是 KSPTableSection 的用途。

那么,本质是什么呢?

  • 与其制作一个巨大的通用获取结果控制器,不如制作两个:一个针对使用平对象存储库的 NSTableView,另一个带有分区支持,使用层次结构存储库的 NSOutlineView
  • 放弃 NSFetchedResultsController 的索引路径概念,因为 NSOutlineView 使用完全不同的 API(-numberOfChildrenOfItem:/-child:ofItem:)。
  • 使输出获取结果控制器集合完全具备集合-KVO 兼容性,因为这就是真正的 Cocoa apps™ 所使用的。

KSPFetchedResultsController

这个类是为了使用 NSTableView 数据源而设计的。

您可以使用一个获取请求和一个托管对象上下文来实例化一个 KSPFetchedResultsController 实例。在需要执行获取时,FRC 的 fetchedObjects 属性会填充 NSManagedObject。获取结果控制器会监听上下文更改通知,并以集合-KVO 兼容的方式更新其 fetchedObjects 集合。为了向 NSTableView 发出粒度更新的请求,您必须成为获取结果控制器的代理,并在您的控制器对象(很有可能是自定义的 NSViewController 子类)中实现 KSPFetchedResultsControllerDelegate 协议。

这通常看起来是这样的

#pragma mark - KSPFetchedResultsControllerDelegate Implementation

- (void) controllerWillChangeContent: (KSPFetchedResultsController*) controller
{
  [self.tableView beginUpdates];
}

- (void) controller: (KSPFetchedResultsController*) controller didChangeObject: (NSManagedObject*) anObject atIndex: (NSUInteger) index forChangeType: (KSPFetchedResultsChangeType) type newIndex: (NSUInteger) newIndex
{
  switch(type)
  {
    case KSPFetchedResultsChangeInsert:
    {
      [self.tableView insertRowsAtIndexes: [NSIndexSet indexSetWithIndex: newIndex] withAnimation: NSTableViewAnimationEffectNone];

      break;
    }

    case KSPFetchedResultsChangeDelete:
    {
      [self.tableView removeRowsAtIndexes: [NSIndexSet indexSetWithIndex: index] withAnimation: NSTableViewAnimationEffectNone];

      break;
    }

    case KSPFetchedResultsChangeMove:
    {
      [self.tableView moveRowAtIndex: index toIndex: newIndex];

      break;
    }

    case KSPFetchedResultsChangeUpdate:
    {
      {{
        NSIndexSet* rowIndexes = [NSIndexSet indexSetWithIndex: index];

        NSIndexSet* columnIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, self.tableView.tableColumns.count)];

        [self.tableView reloadDataForRowIndexes: rowIndexes columnIndexes: columnIndexes];
      }}

      break;
    }
  }
}

- (void) controllerDidChangeContent: (KSPFetchedResultsController*) controller
{
  [self.tableView endUpdates];
}

KSPSectionedFetchedResultsController

这个类是为了使用 NSOutlineView 数据源而设计的。

KSPSectionedFetchedResultsController 继承自 KSPFetchedResultsController 并向后者添加了分区管理功能。

就像使用KSPFetchedResultsController一样,您需要使用一个fetch请求和一个管理对象上下文实例化一个KSPSectionedFetchedResultsController实例。另外,您还需要提供用于将对象分组到组的分区名称键路径。当您进行一次fetch操作时,SFRC的sections属性会填充KSPTableSection实例。获取结果控制器会监听上下文的更改通知,并以KVO兼容的方式更新其sections集合。要向NSOutlineView发出细粒度更新,您必须成为分区获取结果控制器的代理,并在您的控制器对象中实现KSPSectionedFetchedResultsControllerDelegate协议(最可能的是一个自定义的NSViewController子类)。

这通常看起来是这样的

#pragma mark - KPSectionedFetchedResultsControllerDelegate Implementation

- (void) controllerWillChangeContent: (KSPFetchedResultsController*) controller
{
  [self.outlineView beginUpdates];
}

- (void) controller: (KSPSectionedFetchedResultsController*) controller didChangeObject: (NSManagedObject*) anObject atIndex: (NSUInteger) index inSection: (KSPTableSection*) section forChangeType: (KSPFetchedResultsChangeType) type newIndex: (NSUInteger) newIndex inSection: (KSPTableSection*) newSection
{
  switch(type)
  {
    case KSPFetchedResultsChangeInsert:
    {
      [self.outlineView insertItemsAtIndexes: [NSIndexSet indexSetWithIndex: newIndex] inParent: newSection withAnimation: NSTableViewAnimationEffectNone];

      break;
    }

    case KSPFetchedResultsChangeDelete:
    {
      [self.outlineView removeItemsAtIndexes: [NSIndexSet indexSetWithIndex: index] inParent: section withAnimation: NSTableViewAnimationEffectFade];

      break;
    }

    case KSPFetchedResultsChangeMove:
    {
      [self.outlineView moveItemAtIndex: index inParent: section toIndex: newIndex inParent: newSection];

      break;
    }

    case KSPFetchedResultsChangeUpdate:
    {
      [self.outlineView reloadItem: anObject reloadChildren: NO];

      break;
    }
  }
}

- (void) controller: (KSPSectionedFetchedResultsController*) controller didChangeSection: (KSPTableSection*) section atIndex: (NSUInteger) index forChangeType: (KSPSectionedFetchedResultsChangeType) type newIndex: (NSUInteger) newIndex
{
  switch(type)
  {
    case KSPSectionedFetchedResultsChangeInsert:
    {
      [self.outlineView insertItemsAtIndexes: [NSIndexSet indexSetWithIndex: newIndex] inParent: nil withAnimation: NSTableViewAnimationEffectNone];

      break;
    }

    case KSPSectionedFetchedResultsChangeDelete:
    {
      [self.outlineView removeItemsAtIndexes: [NSIndexSet indexSetWithIndex: index] inParent: nil withAnimation: NSTableViewAnimationEffectNone];

      break;
    }

    case KSPSectionedFetchedResultsChangeMove:
    {
      [self.outlineView moveItemAtIndex: index inParent: nil toIndex: newIndex inParent: nil];

      break;
    }
  }
}

- (void) controllerDidChangeContent: (KSPFetchedResultsController*) controller
{
  [self.outlineView endUpdates];
}

KSPMirroredSectionedFetchedResultsController

这个类是为了使用 NSOutlineView 数据源而设计的。

KSPMirroredSectionedFetchedResultsController继承自KSPSectionedFetchedResultsController。它唯一的责任是反转嵌套对象的排序顺序。在某些情况下,比如在您的NSOutlineView中实现“下拉加载更多”功能时,这可能会更高效、更有逻辑。

故意省略的内容

KSPFetchedResultsController没有缓存概念,这在NSFetchedResultsController中是存在的。

KSPFetchedResultsController也没有在NSFetchedResultsController中实现的操作模式概念。

已知问题

NSFetchedResultsController有一个长期的缺陷(也可能是特性?),当你修改NSFetchRequestfetchLimitfetchOffset属性时会出现这个缺陷。例如,当你请求CoreData存储按照特定标准排序并带有偏移量10和限制10的对象时,你最初得到正确的结果。但是,在新的NSManagedObject被插入上下文并通过谓词后,它会被立即报告给FRC的代理作为已插入的对象,尽管它可能实际上不属于跳过前10个对象并排除后面10个的“窗口”。

KSPFetchedResultsController遵循这种行为以与iOS FRC版本兼容。

就是这些!告诉我您对此的看法。