这是苹果官方文档 Core Data Programming Guide 的渣翻译。
在OS X系统中,Core Data设计目的在于通过Cocoa bindings在用户接口发挥作用。然而,Cocoa bindings并不是iOS用户接口的一部分。在iOS中,你需要使用NSFetchedResultsController去连接模型(Core Data)和视图(storyboard)。
NSFetchedResultsController提供了Core Data和UITableView对象之间的接口。因为table视图是在iOS中用得最多的用以展示数据的方式,UITableView几乎处理了所有大量数据集合的显示。
创建一个Fetched Result Controller
一般来讲,一个NSFetchedResultsController实例会被一个UITableViewController实例在需要的时候初始化。这个初始化过程可以发生在viewDidLoad或者viewWillAppear方法,或者在其他视图控制器任意一个生命周期中某个时间点。下面这个例子展示了NSFetchedResultsController的初始化。
OBJECTIVE-C
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
- (void)initializeFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSSortDescriptor *lastNameSort = [NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES];
[request setSortDescriptors:@[lastNameSort]];
NSManagedObjectContext *moc = …; //Retrieve the main queue NSManagedObjectContext
[self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:nil cacheName:nil]];
[[self fetchedResultsController] setDelegate:self];
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(@"Failed to initialize FetchedResultsController: %@\n%@", [error localizedDescription], [error userInfo]);
abort();
}
}
SWIFT
var fetchedResultsController: NSFetchedResultsController!
func initializeFetchedResultsController() {
let request = NSFetchRequest(entityName: "Person")
let departmentSort = NSSortDescriptor(key: "department.name", ascending: true)
let lastNameSort = NSSortDescriptor(key: "lastName", ascending: true)
request.sortDescriptors = [departmentSort, lastNameSort]
let moc = self.dataController.managedObjectContext
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: "rootCache")
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
}
在上述的、在UITableViewController实例运行的时候能一直存活的initializeFetchedResultsController方法中,你第一步构建了一个Fetch请求(NSFetchRequest),Fetch请求是这个NSFetchedResultsController的中心。注意这个Fetch请求包含了一个排序描述器(NSSortDescriptor)。NSFetchedResultsController需要至少一个排序描述器来控制展示出来的数据顺序。
一旦Fetch请求被初始化,你就可以初始化NSFetchedResultsController实例了。Fetched Results Controller需要你传递一个NSFetchRequest实例和一个托管对象上下文实例(NSManagedObjectContext)来运行操作。sectionNameKeyPath和cacheName属性都是可选的。
一旦Fetched Results Controller初始化了,你就可以设置它的代理(delegate)。这个代理会通知table view有关任何的数据结构的变化。一般来说,table view同时也是Fetched Results Controller的代理,所以能够在相关数据发生变化的时候调用回调。
下一步,你可以开始调用performFetch:来使用NSFetchedResultsController。这个调用会查询初始数据用以展示,并触发NSFetchedResultsController实例开始监控托管对象上下文MOC的变化。
集成Fetched Results Controller 和 Table View Data Source
在你集成了已初始化的fetched results controller和准备好了要展示在table view上的数据之后,你需要集成fetched results controller和table view的data source(UITableViewDataSource)。
OBJECTIVE-C
#pragma mark - UITableViewDataSource
- (void)configureCell:(id)cell atIndexPath:(NSIndexPath*)indexPath
{
id object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
// Populate cell from the NSManagedObject instance
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id cell = [tableView dequeueReusableCellWithIdentifier:CellReuseIdentifier];
// Set up the cell
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[[self fetchedResultsController] sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id< NSFetchedResultsSectionInfo> sectionInfo = [[self fetchedResultsController] sections][section];
return [sectionInfo numberOfObjects];
}
SWIFT
func configureCell(cell: UITableViewCell, indexPath: NSIndexPath) {
let employee = fetchedResultsController.objectAtIndexPath(indexPath) as! AAAEmployeeMO
// Populate cell from the NSManagedObject instance
print("Object for configuration: \(object)")
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier", forIndexPath: indexPath) as! UITableViewCell
// Set up the cell
configureCell(cell, indexPath: indexPath)
return cell
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return fetchedResultsController.sections!.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sections = fetchedResultsController.sections as! [NSFetchedResultsSectionInfo]
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
就如上面每个UITableViewDataSource的方法展示的那样,fetched results controller的集成接口减少到了只需要一个就能跟table view data source进行集成了。
传递数据更新到Table View
除了更加容易集成Core Data和table view data source之外,NSFetchedResultsController还能在数据发生改变的时候跟UITableViewController实例通信。为了实现这个功能,需要实现 NSFetchedResultsControllerDelegate协议:
OBJECTIVE-C
#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type) {
case NSFetchedResultsChangeInsert:
[[self tableView] insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[[self tableView] deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
case NSFetchedResultsChangeUpdate:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type) {
case NSFetchedResultsChangeInsert:
[[self tableView] insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[[self tableView] cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[[self tableView] insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[[self tableView] endUpdates];
}
SWIFT
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Insert:
tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Delete:
tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
case .Move:
break
case .Update:
break
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: NSManagedObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
case .Update:
configureCell(self.tableView.cellForRowAtIndexPath(indexPath!)!, indexPath: indexPath!)
case .Move:
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
实现这4个上述的协议方法就能够自动更新UITableView,无论何时相关数据的更新都能够触发更新。
添加Sections
到目前为止你可以已经使用一个仅有一个sectiion的table view,这个setion展示了所有table view需要展示的数据。如果你要操作拥有更多数据的Employee对象集合,那么把这个table view分成多个section更好。把Employee按照department来分组更加利于管理。如果不使用Core Data,实现一个带有多个section的table view可以解析一个含有多个数组的数组或更加复杂的数据结构。有了Core Data,你仅需要对fetched results controller的构建做出一点小修改即可.
OBJECTIVE-C
- (void)initializeFetchedResultsController
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
NSSortDescriptor *departmentSort = [NSSortDescriptor sortDescriptorWithKey:@"department.name" ascending:YES];
NSSortDescriptor *lastNameSort = [NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES];
[request setSortDescriptors:@[departmentSort, lastNameSort]];
NSManagedObjectContext *moc = [[self dataController] managedObjectContext];
[self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:@"department.name" cacheName:nil]];
[[self fetchedResultsController] setDelegate:self];
SWIFT
func initializeFetchedResultsController() {
let request = NSFetchRequest(entityName: "Person")
let departmentSort = NSSortDescriptor(key: "department.name", ascending: true)
let lastNameSort = NSSortDescriptor(key: "lastName", ascending: true)
request.sortDescriptors = [departmentSort, lastNameSort]
let moc = dataController.managedObjectContext
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
}
在这个例子中你需要添加多一个NSSortDesctiptor实例到NSFetchRequest实例中。你要在NSFetchedResultsController的初始化中为sectionNameKeyPath参数设置跟这个新sort descriptor同样的key。fetched results controller使用这个初始化的controller并把数据分成多个section,因此所有的key都要匹配。
这个修改让fetched results controller把返回的Person实例基于每个Person相关的department的name分成了多个section。唯一使用这个特性的条件是:
- sectionNameKeyPath属性必须是一个NSSortDescriptor实例。
- 上述NSSortDescriptor必须是传递给fetch request的数组中的第一个descriptor。
为性能而添加的缓存
在许多情况下,一个table view会表示同一种相近类型的数据。一个fetch request在table view controller创建的时候被定义,并且在应用的生命周期内不会改变。如果能给NSFetchedResultsController实例添加一个缓存,当应用重新启动而且数据没有改变的时候,这无疑是极好的,这样table view能迅速初始化。特别是对大型数据集,缓存特别有用。
OBJECTIVE-C
[self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:@"department.name" cacheName:@"rootCache"]];
SWIFT
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: "rootCache")
就如上面展示的,cacheName参数在NSFetchedResultsController实例初始化的时候设置,fetched result controller会自动获得这个缓存。之后数据的加载几乎是瞬间的事。
注意
如果一个fetched results controller相关的fetch request要发生改变,很重要一点就是在fetched results controller发生改变之前缓存必须无效化。你可以使用NSFetchedResultsController的类方法deleteCacheWithName:来无效化缓存。