NSFetchedResultsController: выборка в фоновом потоке
У меня есть более или менее основной UITableViewController
с NSFetchedResultsController
, UITableViewController
выталкивается на navigationController's
стек. Но анимация толчка не гладкая, потому что выборка NSFetchedResultsController
выполняется в основном потоке, и, следовательно, блокирует пользовательский интерфейс.
Мой вопрос: как я могу выполнить выборку NSFetchedResultsController
в фоновом потоке, чтобы анимация была плавной?
NSFetchedResultsController
и методы делегата выглядят так:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:@"GPGrade" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
//Set predicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"parent == %@", self.subject];
[fetchRequest setPredicate:predicate];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSArray *sortDescriptors = @[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"SubjectMaster"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
- (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;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationTop];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeUpdate:
//[self configureCell:(GPSubjectOverviewListCell *)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
4 ответа
Общее правило с базовыми данными - один контекст управляемого объекта на поток и один поток на MOC. Имея это в виду, вам нужно выполнить выборку для Контроллера полученных результатов в главном потоке, так как это поток, который будет взаимодействовать с управляемыми объектами FRC. (См. Руководство по программированию основных данных - параллелизм с основными данными)
Если у вас возникают проблемы с производительностью анимации, вам, вероятно, следует поискать способы обеспечить выборку до или после нажатия на представление. Обычно вы выполняете выборку в контроллере вида viewDidLoad:
и навигационный контроллер не будет выдвигать представление, пока выборка не будет завершена.
TL;DR; Нет веской причины использовать контекст в главной очереди.
можно использовать NSFetchedResultsController для извлечения данных в фоновом режиме
Абсолютно. NSFetchedResultsController
может использоваться с частным контекстом очереди. Это, на самом деле, довольно радостно и продуктивно при этом. Есть ошибка, которая мешает NSFetchedResultsController
от использования его кеша, когда он использует приватную очередь, но кеш не выигрывает так много, как в iOS 3.0. Установить cacheName
из ноля, и вы будете в порядке.
1.
Создать контекст с NSPrivateQueueConcurrencyType
, Желательно не тот, который вы используете для ввода-вывода.
2.
Создайте полученный контроллер результатов с этим контекстом и именем кеша nil.
3.
Выполните начальную выборку изнутри performBlock:
блок:
[[[self fetchedResultsController] managedObjectContext] performBlock:^{
NSError *fetchError = nil;
if (![self fetchedResultsController] performFetch:&error]){
/// handle the error. Don't just log it.
} else {
// Update the view from the main queue.
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[tableView reloadData];
}];
}
}];
4.
Все ваши обратные вызовы делегатов теперь будут происходить из очереди контекста. Если вы используете их для обновления представлений, сделайте это, отправив в основную очередь, как показано выше.
5.
...
6.
Прибыль!
Вы можете прочитать больше об этом здесь.
Вы можете пройти этот очень хороший пост о Core Data с многопоточным поведением.
Надеюсь, поможет..!!
Сегодня работал над этой проблемой. Хорошо работающее решение — обернуть NSFetchedResultsController(FRC) в операцию (FRCOperation). Целью этой операции после завершения является удержание FRC и проксирование его.
// After completing the operation, it will proxy calls
// from "NSFetchedResultsControllerDelegate.controller(_:didChangeContentWith:)"
protocol FetchOperationDelegate {
func didChangeContent(snapshot: NSDiffableDataSourceSnapshotReference)
}
// Exemple Operation
protocol FetchOperation: Operation, NSFetchedResultsControllerDelegate {
weak var delegate: FetchOperationDelegate { get set }
// To get objects from NSFetchedResultsController. Calls always on the main thread
func object(at indexPath: IndexPath) -> NSManagedObject?
}
Логика:
- Открытие контроллера
- Сначала создайте FetchOperation. Не забудьте установить FetchOperation.delegate = controller. Потому что после выполнения вы должны получить первые данные в FetchOperationDelegate.
- Сохраните эту FetchOperation в свойстве контроллера (viewModel).
- Выполнить FetchOperation. NSFetchedResultsController.performFetch в фоновом режиме. Не забудьте использовать фоновый контекст.
- Например, через 5 секунд FetchOperation завершена.
- Обновите свой контроллер, взяв данные из операции
- С этого момента. Вы должны получать обновления от FRC через FetchOperationDelegate(NSFetchedResultsControllerDelegate). И вы также можете получить доступ к объектам из FRC в основном потоке.
Если вам нужно изменить предикат или sort. Вам просто нужно создать новый FetchOperation. Пока выполняется новая операция, вы продолжаете работать с последней версией FetchOperation.