iOS, CoreData и NSFetchedResultsController

в 4:34, , рубрики: core data, iOS, разработка под iOS, метки: ,

Приветствую вас, друзья.

Если вы программируете под 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js