Приветствую вас, друзья.
Если вы программируете под iOS и в своей работе используете CoreData, то скорее всего вы сталкивались с классом NSFetchedResultsController. В данном посте я хочу поговорить о нем и не только.
Начнем немного издалека и давайте вспомним что же такое CoreData и какие есть основные классы в данном фреймворке.
CoreData — это технология и одноименный фреймворк от Apple позволяющие работать с графом объектов. Физическим хранилищем объектов может быть как база SQLite, так и XML файл или программист может создать свое хранилище.
NSPersistentStoreCoordinator — класс, отвечающий за взаимодействие с хранилищем.
NSManagedObjectModel — класс, отвечающий за доступ к модели данных (структура данных, описание свойств и т.д.)
NSManagedObjectContext — контекст данных. Я бы сказал что это обособленный граф данных из хранилища. Контекстов в программе может быть несколько. Если у вас несколько потоков, не важно как вы их реализуете, в виде NSOperation, блоков или NSThread, то на каждый поток создается отдельный контекст и передача объектов между потоками запрещена, вы можете передать только объект класса NSManagedObjectId
NSManagedObject — базовый класс для всех объектов хранящихся в CoreData.
Данные сами по себе может быть и представляют какую-либо ценность, но, обычно их нужно использовать. Одним из элементов представления данных в iOS служат таблицы (объекты класса UITableView), которые через объект класса NSFetchedResultsController можно привязать к CoreData. После этого при изменении данных в CoreData будет актуализироваться информация в таблице. Так же, с помощью таблицы можно управлять данными в хранилище.
Со вступлением закончено, теперь переходим к делу. NSFetchedResultsController (в дальнейшем FRC, объект или класс — будет зависеть от контекста, но чаще всего объект) — контроллер результатов выборки. Создается, обычно один экземпляр на ViewController, но вполне может работать и без оного, внутрь которого помещается исключительно для того, что бы было проще привязать данные к виду. Если быть более точным, то вспомним, что для работы таблиц нужно определить методы протокола UITableViewDataSource и UITableViewDelegate, выше мной подразумевалось что это сделано прямо в контроллере вида.
@interface YourViewClass:UIViewController <UITableViewSource, UITableViewDelegate, NSFetchedResultsControllerDelegate>
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
@end
@implementation YourViewClass
@synthesize fetchedResultsController = _fetchedResultsController;
@end
Использование FRC возможно и без объявления его как свойства класса, но тогда объект придется создавать где-либо в другом месте, а тут код self.fetchedResultsController в методах вызывает метод, который можно определить, например, так:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
return _fetchedResultsController;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// …
// Настройка fetchRequest
// …
NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
frc.delegate = self;
_fetchedResultsController = frc;
NSError __autoreleasing *error = nil;
if (![_fetchedResultsController performFetch:&error]) {
// Обработка ошибок
}
return _fetchedResultsController;
}
Обратите внимание на строку
frc.delegate = self;
При изменении каких либо данных в выборке, хранящейся в frc, делегат об этом уведомляется. Протокол NSFetchedResultsControllerDelegate содержит набор методов, определив которые вы сможете обрабатывать произошедшие изменения, внося сооответствующие поправки в интерфейс.
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch (type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationTop];
break;
case NSFetchedResultsChangeUpdate:
// Обновляем
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationBottom];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
default:
break;
}
}
-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
Код выше — это один из примеров реализации делегата для FRC. В данном подходе есть только один маленький минус, с которым мне как-то пришлось столкнуться, а именно: NSFetchRequest позволяет работать только с одной сущностью и это почти во всех случаях оправдано, но вполне может возникнуть ситуация когда выборки из одной сущности может оказаться недостаточно, а наследование не получится сделать. Как же тогда быть?
Каждый FRC связан с одним объектом NSManagedObjectContext (кстати, работать FRC может только в главном потоке, это обусловлено тем, что интерфейс так же работает в нем и там же вызываются все методы делегата). А все контексты, при изменении в них данных, отправляют нотификацию NSManagedObjectContextObjectsDidChangeNotification. При этом в нотификации, в свойстве object хранится отправитель нотификации, а в свойстве userInfo хранится словарь, в словаре утка, в утке яйцо и т.д. же хранятся измененные объекты в виде массивов, которые доступны по ключам NSInsertedObjectsKey, NSUpdatedObjectsKey, andNSDeletedObjectsKey. Собственно, все эти объекты обрабатываются, о результатах обработки уведомляется делегат, финальные результаты складируются во внутреннем хранилище и все довольны. Но вернемся к ситуации с выборкой из нескольких сущностей, или к ситуации, когда нам нужно отслеживать изменения в фоновом потоке. FRC в данном случае умывает руки, а мы создаем хранилище для объектов, не важно, словарь это или массив. Заполняем его изначальными данными, а затем регистрируем наш объект как обсервер для того контекста, с которым мы работаем.
Ниже пример для данных, которые выбраны из 3-х сущностей и разделены на 3 секции, соответственно в представлении. Для этого все данные сохраняются в словарь, где один ключ соответствует одной секции, а значение по ключу — это массив с объектами, при этом объекты не добавляются и не удаляются, могут измениться только их параметры.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onChangeManagedObjectsContext:) name:NSManagedObjectContextObjectsDidChangeNotification object:managedObjectContext];
-(void) onChangeManagedObjectsContext:(NSNotification *)notification {
if (!_observingStarted)
return;
NSArray *updatedObjects = [notification.userInfo valueForKey:@"updated"];
for (NSManagedObject *object in updatedObjects) {
@autoreleasepool {
NSIndexPath *indexPath = nil;
if ([object isKindOfClass:[OurClass1 class]]) {
NSUInteger section = [_sections indexOfObject:@"ourClass1"];
NSArray *objects = [_loadedObjects valueForKey:@"ourClass1"];
if (objects == nil)
continue;
NSUInteger row = [objects indexOfObjectIdenticalTo:object];
if (row == NSNotFound)
continue;
indexPath = [NSIndexPath indexPathForRow:row inSection:section];
}
else if ([object isKindOfClass:[OurClass2 class]]) {
NSUInteger section = [_sections indexOfObject:@"ourClass2”];
NSArray *objects = [_loadedObjects valueForKey:@"ourClass2"];
if (objects == nil)
continue;
NSUInteger row = [objects indexOfObjectIdenticalTo:object];
if (row == NSNotFound)
continue;
indexPath = [NSIndexPath indexPathForRow:row inSection:section];
}
else if ([object isKindOfClass:[OurClass3 class]]) {
NSUInteger section = [_sections indexOfObject:@"ourClass3"];
NSArray *objects = [_loadedObjects valueForKey:@"ourClass3"];
if (objects == nil)
continue;
NSUInteger row = [objects indexOfObjectIdenticalTo:object];
if (row == NSNotFound)
continue;
indexPath = [NSIndexPath indexPathForRow:row inSection:section];
}
[self didChangeObject:object atIndexPath:indexPath];
}
}
}
- (void)didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath {
if (!_observingStarted)
return;
if ([anObject isKindOfClass:[OurClass1 class]]) {
if (![[self.tableView indexPathsForVisibleRows] containsObject:indexPath])
return;
// А тут вносим необходимые поправки в интерфейс
}
}
На самом деле, работа NSFetchedResultsController намного сложнее, так как там присутствуют сортировки, реализовано кеширование объектов, ничего подобного в коде выше нет за ненадобностью, но общую картину я постарался прояснить и, очень надеюсь, что ВАМ это поможет.
Автор: newonder