Статьи

iOS 8: базовые данные и асинхронная выборка

В предыдущей статье об iOS 8 и Core Data мы обсуждали пакетные обновления. Пакетные обновления — не единственный новый API в городе. Начиная с iOS 8 и OS X Yosemite, возможно асинхронное получение данных. В этом руководстве мы подробнее рассмотрим, как реализовать асинхронную выборку и в каких ситуациях ваше приложение может извлечь выгоду из этого нового API.

Как и пакетные обновления, асинхронная выборка уже давно входит в список пожеланий многих разработчиков. Запрос на выборку может быть сложным, и на его выполнение уходит нетривиальное количество времени. В течение этого времени запрос на выборку блокирует поток, в котором он работает, и в результате блокирует доступ к контексту управляемого объекта, выполняющего запрос на выборку. Проблема проста для понимания, но как выглядит решение Apple.

Ответ Apple на эту проблему — асинхронная загрузка. Асинхронный запрос на выборку выполняется в фоновом режиме. Это означает, что он не блокирует другие задачи во время выполнения, такие как обновление пользовательского интерфейса в главном потоке.

Асинхронная выборка также имеет две другие удобные функции: отчеты о прогрессе и отмену. Асинхронный запрос на выборку может быть отменен в любое время, например, когда пользователь решает, что запрос на выборку занимает слишком много времени. Отчеты о ходе выполнения — полезное дополнение, показывающее пользователю текущее состояние запроса на выборку.

Асинхронная выборка — это гибкий API. Мало того, что можно отменить запрос асинхронной выборки, но также можно вносить изменения в контекст управляемого объекта во время выполнения запроса асинхронной выборки. Другими словами, пользователь может продолжать использовать ваше приложение, пока приложение выполняет асинхронный запрос на выборку в фоновом режиме.

Как и пакетные обновления, асинхронные запросы на выборку передаются в контекст управляемого объекта как объект NSPersistentStoreRequest , NSPersistentStoreRequest экземпляр класса NSAsynchronousFetchRequest .

Экземпляр NSAsynchronousFetchRequest инициализируется объектом NSFetchRequest и блоком завершения. Блок завершения выполняется, когда асинхронный запрос на выборку завершил свой запрос на выборку.

Давайте вернемся к приложению, которое мы создали ранее в этой серии, и заменим текущую реализацию класса NSFetchedResultsController асинхронным запросом выборки.

Загрузите или клонируйте проект из GitHub и откройте его в Xcode 6. Прежде чем мы сможем начать работать с классом NSAsynchronousFetchRequest , нам нужно внести некоторые изменения. Мы не сможем использовать класс NSFetchedResultsController для управления данными табличного представления, поскольку класс NSFetchedResultsController был разработан для работы в основном потоке.

Начните с обновления расширения частного класса класса TSPViewController как показано ниже. Мы удаляем свойство fetchedResultsController и создаем новое свойство items типа NSArray для хранения items NSArray дел. Это также означает, что класс TSPViewController больше не должен соответствовать протоколу NSFetchedResultsControllerDelegate .

1
2
3
4
5
6
7
@interface TSPViewController ()
 
@property (strong, nonatomic) NSArray *items;
 
@property (strong, nonatomic) NSIndexPath *selection;
 
@end

Прежде чем мы проведем рефакторинг метода viewDidLoad , я сначала хочу обновить реализацию протокола UITableViewDataSource . Посмотрите на изменения, которые я сделал в следующих блоках кода.

1
2
3
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.items ?
}
1
2
3
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.items ?
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (void)configureCell:(TSPToDoCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // Fetch Record
    NSManagedObject *record = [self.items objectAtIndex:indexPath.row];
     
    // Update Cell
    [cell.nameLabel setText:[record valueForKey:@»name»]];
    [cell.doneButton setSelected:[[record valueForKey:@»done»] boolValue]];
     
    [cell setDidTapButtonBlock:^{
        BOOL isDone = [[record valueForKey:@»done»] boolValue];
         
        // Update Record
        [record setValue:@(!isDone) forKey:@»done»];
    }];
}
1
2
3
4
5
6
7
8
9
— (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSManagedObject *record = [self.items objectAtIndex:indexPath.row];
         
        if (record) {
            [self.managedObjectContext deleteObject:record];
        }
    }
}

Нам также нужно изменить одну строку кода в prepareForSegue:sender: как показано ниже.

1
2
// Fetch Record
NSManagedObject *record = [self.items objectAtIndex:self.selection.row];

И последнее, но не менее важное: удалите реализацию протокола NSFetchedResultsControllerDelegate , поскольку он нам больше не нужен.

Как вы можете видеть ниже, мы создаем асинхронный запрос на viewDidLoad методе viewDidLoad контроллера представления. Давайте на минутку посмотрим, что происходит.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
— (void)viewDidLoad {
    [super viewDidLoad];
     
    // Helpers
    __weak TSPViewController *weakSelf = self;
     
    // Initialize Fetch Request
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@»TSPItem»];
     
    // Add Sort Descriptors
    [fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@»createdAt» ascending:YES]]];
     
    // Initialize Asynchronous Fetch Request
    NSAsynchronousFetchRequest *asynchronousFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult *result) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // Process Asynchronous Fetch Result
            [weakSelf processAsynchronousFetchResult:result];
        });
    }];
     
    // Execute Asynchronous Fetch Request
    [self.managedObjectContext performBlock:^{
        // Execute Asynchronous Fetch Request
        NSError *asynchronousFetchRequestError = nil;
        NSAsynchronousFetchResult *asynchronousFetchResult = (NSAsynchronousFetchResult *)[weakSelf.managedObjectContext executeRequest:asynchronousFetchRequest error:&asynchronousFetchRequestError];
         
        if (asynchronousFetchRequestError) {
            NSLog(@»Unable to execute asynchronous fetch result.»);
            NSLog(@»%@, %@», asynchronousFetchRequestError, asynchronousFetchRequestError.localizedDescription);
        }
    }];
}

Мы начнем с создания и настройки экземпляра NSFetchRequest для инициализации асинхронного запроса на выборку. Именно этот запрос выборки будет выполняться в фоновом режиме асинхронным запросом выборки.

1
2
3
4
5
// Initialize Fetch Request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@»TSPItem»];
 
// Add Sort Descriptors
[fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@»createdAt» ascending:YES]]];

Чтобы инициализировать экземпляр NSAsynchronousFetchRequest , мы вызываем initWithFetchRequest:completionBlock: fetchRequest initWithFetchRequest:completionBlock: передавая fetchRequest и блок завершения.

1
2
3
4
5
6
7
// Initialize Asynchronous Fetch Request
NSAsynchronousFetchRequest *asynchronousFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult *result) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // Process Asynchronous Fetch Result
        [weakSelf processAsynchronousFetchResult:result];
    });
}];

Блок завершения вызывается, когда асинхронный запрос на выборку завершил выполнение своего запроса на выборку. Блок завершения принимает один аргумент типа NSAsynchronousFetchResult , который содержит результат запроса, а также ссылку на исходный запрос асинхронной выборки.

В блоке завершения мы вызываем processAsynchronousFetchResult: передавая объект NSAsynchronousFetchResult . Мы рассмотрим этот вспомогательный метод через несколько минут.

Выполнение асинхронного запроса на выборку практически идентично тому, как мы выполняем NSBatchUpdateRequest . Мы вызываем executeRequest:error: в контексте управляемого объекта, передавая асинхронный запрос выборки и указатель на объект NSError .

01
02
03
04
05
06
07
08
09
10
[self.managedObjectContext performBlock:^{
    // Execute Asynchronous Fetch Request
    NSError *asynchronousFetchRequestError = nil;
    NSAsynchronousFetchResult *asynchronousFetchResult = (NSAsynchronousFetchResult *)[weakSelf.managedObjectContext executeRequest:asynchronousFetchRequest error:&asynchronousFetchRequestError];
     
    if (asynchronousFetchRequestError) {
        NSLog(@»Unable to execute asynchronous fetch result.»);
        NSLog(@»%@, %@», asynchronousFetchRequestError, asynchronousFetchRequestError.localizedDescription);
    }
}];

Обратите внимание, что мы выполняем асинхронный запрос на выборку, вызывая performBlock: в контексте управляемого объекта. Хотя это не является строго необходимым, поскольку метод viewDidLoad , в котором мы создаем и выполняем асинхронный запрос выборки, вызывается в основном потоке, это хорошая привычка и лучшая практика.

Даже если асинхронный запрос на выборку выполняется в фоновом режиме, обратите внимание, что метод executeRequest:error: возвращается немедленно, передавая нам объект NSAsynchronousFetchResult . Как только асинхронный запрос на выборку завершается, тот же объект NSAsynchronousFetchResult заполняется результатом запроса на выборку.

Наконец, мы проверяем, был ли запрос асинхронной выборки выполнен без проблем, проверяя, равен ли объект NSError nil .

processAsynchronousFetchResult: метод — не более чем вспомогательный метод, в котором мы обрабатываем результат асинхронного запроса на выборку. Мы устанавливаем свойство items контроллера представления с содержимым свойства finalResult результата и перезагружаем табличное представление.

1
2
3
4
5
6
7
8
9
— (void)processAsynchronousFetchResult:(NSAsynchronousFetchResult *)asynchronousFetchResult {
    if (asynchronousFetchResult.finalResult) {
        // Update Items
        [self setItems:asynchronousFetchResult.finalResult];
         
        // Reload Table View
        [self.tableView reloadData];
    }
}

Создайте проект и запустите приложение в iOS Simulator. Вы можете быть удивлены, увидев сбой вашего приложения, когда оно пытается выполнить асинхронный запрос на выборку. К счастью, вывод в консоли говорит нам, что пошло не так.

1
2
3
*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘NSConfinementConcurrencyType context <NSManagedObjectContext: 0x7fce3a731e60> cannot support asynchronous fetch request <NSAsynchronousFetchRequest: 0x7fce3a414300> with fetch request <NSFetchRequest: 0x7fce3a460860> (entity: TSPItem; predicate: ((null)); sortDescriptors: ((
    «(createdAt, ascending, compare:)»
));

Если вы не читали статью о Core Data и параллелизме , вы можете быть озадачены тем, что читаете. Помните, что Core Data объявляет три типа параллелизма: NSConfinementConcurrencyType , NSPrivateQueueConcurrencyType и NSMainQueueConcurrencyType . Всякий раз, когда вы создаете контекст управляемого объекта, вызывая метод init класса, тип параллелизма результирующего контекста управляемого объекта равен NSConfinementConcurrencyType . Это тип параллелизма по умолчанию.

Проблема, однако, заключается в том, что асинхронная выборка несовместима с типом NSConfinementConcurrencyType . Не вдаваясь в подробности, важно знать, что асинхронный запрос на выборку должен объединить результаты своего запроса на выборку с контекстом управляемого объекта, который выполнил запрос на асинхронную выборку. Он должен знать, в какой очереди отправки он может это сделать, и именно поэтому только NSPrivateQueueConcurrencyType и NSMainQueueConcurrencyType поддерживают асинхронную NSMainQueueConcurrencyType . Решение очень простое, хотя.

Откройте TSPAppDelegate.m и обновите метод managedObjectContext как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
— (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext) {
        return _managedObjectContext;
    }
     
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
     
    if (coordinator) {
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
     
    return _managedObjectContext;
}

Единственное изменение, которое мы сделали, — это замена метода init на initWithConcurrencyType: передав в качестве аргумента NSMainQueueConcurrencyType . Это означает, что контекст управляемого объекта должен быть доступен только из основного потока. Это прекрасно работает, если мы используем performBlock: или performBlockAndWait: для доступа к контексту управляемого объекта.

Запустите проект еще раз, чтобы убедиться, что наше изменение действительно устранило проблему.

Класс NSAsynchronousFetchRequest добавляет поддержку для отслеживания хода выполнения запроса на выборку, и даже можно отменить асинхронный запрос на выборку, например, если пользователь решит, что его выполнение занимает слишком много времени.

Класс NSAsynchronousFetchRequest использует класс NSProgress для отчетов о ходе выполнения, а также для отмены асинхронного запроса на выборку. Класс NSProgress , доступный с iOS 7 и OS X 10.9, представляет собой умный способ отслеживать ход выполнения задачи без необходимости тесно связывать задачу с пользовательским интерфейсом.

Класс NSProgress также поддерживает отмену, что позволяет отменить запрос асинхронной выборки. Давайте выясним, что нам нужно сделать для реализации отчетов о ходе выполнения асинхронного запроса на выборку.

Мы покажем пользователю ход выполнения асинхронного запроса на выборку с помощью библиотеки SVProgressHUD Сэма Верметта . Загрузите библиотеку из GitHub и добавьте папку SVProgressHUD в ваш проект Xcode.

В этой статье мы не NSProgress подробно изучать класс NSProgress , но не стесняйтесь читать больше об этом в документации . Мы создаем экземпляр NSProgress в блоке, который performBlock: методу performBlock: методе viewDidLoad контроллера представления.

1
2
3
4
5
// Create Progress
NSProgress *progress = [NSProgress progressWithTotalUnitCount:1];
 
// Become Current
[progress becomeCurrentWithPendingUnitCount:1];

Вы можете быть удивлены тем, что мы установили общее количество единиц на 1 . Причина проста. Когда Core Data выполняет запрос асинхронной выборки, он не знает, сколько записей он найдет в постоянном хранилище. Это также означает, что мы не сможем показать относительный прогресс пользователю — процент. Вместо этого мы покажем пользователю абсолютный прогресс — количество найденных им записей.

Вы можете устранить эту проблему, выполнив запрос на выборку, чтобы получить количество записей перед выполнением асинхронного запроса на выборку. Я предпочитаю этого не делать, потому что это также означает, что выборка записей из постоянного хранилища занимает больше времени из-за дополнительного запроса на выборку в начале.

Когда мы выполняем асинхронный запрос на выборку, нам немедленно передается объект NSAsynchronousFetchResult . Этот объект имеет свойство progress , которое имеет тип NSProgress . Именно это свойство progress мы должны наблюдать, если хотим получать обновления прогресса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Execute Asynchronous Fetch Request
[self.managedObjectContext performBlock:^{
    // Create Progress
    NSProgress *progress = [NSProgress progressWithTotalUnitCount:1];
     
    // Become Current
    [progress becomeCurrentWithPendingUnitCount:1];
     
    // Execute Asynchronous Fetch Request
    NSError *asynchronousFetchRequestError = nil;
    NSAsynchronousFetchResult *asynchronousFetchResult = (NSAsynchronousFetchResult *)[self.managedObjectContext executeRequest:asynchronousFetchRequest error:&asynchronousFetchRequestError];
     
    if (asynchronousFetchRequestError) {
        NSLog(@»Unable to execute asynchronous fetch result.»);
        NSLog(@»%@, %@», asynchronousFetchRequestError, asynchronousFetchRequestError.localizedDescription);
    }
     
    // Add Observer
    [asynchronousFetchResult.progress addObserver:self forKeyPath:@»completedUnitCount» options:NSKeyValueObservingOptionNew context:ProgressContext];
     
    // Resign Current
    [progress resignCurrent];
}];

Обратите внимание, что мы вызываем resignCurrent для объекта progress чтобы сбалансировать предыдущий becomeCurrentWithPendingUnitCount: Имейте в виду, что оба эти метода должны быть вызваны в одном потоке.

В блоке завершения асинхронного запроса на выборку мы удаляем наблюдателя и закрываем HUD прогресса.

01
02
03
04
05
06
07
08
09
10
11
12
13
// Initialize Asynchronous Fetch Request
NSAsynchronousFetchRequest *asynchronousFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult *result) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // Dismiss Progress HUD
        [SVProgressHUD dismiss];
         
        // Process Asynchronous Fetch Result
        [weakSelf processAsynchronousFetchResult:result];
         
        // Remove Observer
        [result.progress removeObserver:weakSelf forKeyPath:@»completedUnitCount» context:ProgressContext];
    });
}];

Перед тем, как мы реализуем observeValueForKeyPath:ofObject:change:context: нам нужно добавить оператор импорта для библиотеки SVProgressHUD, объявить статическую переменную ProgressContext которую мы передаем в качестве контекста при добавлении и удалении наблюдателя, и показать HUD прогресса перед созданием запрос асинхронной выборки.

1
#import «SVProgressHUD/SVProgressHUD.h»
1
static void *ProgressContext = &ProgressContext;
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
— (void)viewDidLoad {
    [super viewDidLoad];
     
    // Helpers
    __weak TSPViewController *weakSelf = self;
     
    // Show Progress HUD
    [SVProgressHUD showWithStatus:@»Fetching Data» maskType:SVProgressHUDMaskTypeGradient];
     
    // Initialize Fetch Request
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@»TSPItem»];
     
    // Add Sort Descriptors
    [fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@»createdAt» ascending:YES]]];
     
    // Initialize Asynchronous Fetch Request
    NSAsynchronousFetchRequest *asynchronousFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult *result) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // Dismiss Progress HUD
            [SVProgressHUD dismiss];
             
            // Process Asynchronous Fetch Result
            [weakSelf processAsynchronousFetchResult:result];
             
            // Remove Observer
            [result.progress removeObserver:weakSelf forKeyPath:@»completedUnitCount» context:ProgressContext];
        });
    }];
     
    // Execute Asynchronous Fetch Request
    [self.managedObjectContext performBlock:^{
        // Create Progress
        NSProgress *progress = [NSProgress progressWithTotalUnitCount:1];
         
        // Become Current
        [progress becomeCurrentWithPendingUnitCount:1];
         
        // Execute Asynchronous Fetch Request
        NSError *asynchronousFetchRequestError = nil;
        NSAsynchronousFetchResult *asynchronousFetchResult = (NSAsynchronousFetchResult *)[weakSelf.managedObjectContext executeRequest:asynchronousFetchRequest error:&asynchronousFetchRequestError];
         
        if (asynchronousFetchRequestError) {
            NSLog(@»Unable to execute asynchronous fetch result.»);
            NSLog(@»%@, %@», asynchronousFetchRequestError, asynchronousFetchRequestError.localizedDescription);
        }
         
        // Add Observer
        [asynchronousFetchResult.progress addObserver:self forKeyPath:@»completedUnitCount» options:NSKeyValueObservingOptionNew context:ProgressContext];
         
        // Resign Current
        [progress resignCurrent];
    }];
}

Все, что нам осталось сделать, — это реализовать observeValueForKeyPath:ofObject:change:context: метод. Мы проверяем, равен ли context ProgressContext , создаем объект status , извлекая количество выполненных записей из словаря change , и обновляем HUD прогресса. Обратите внимание, что мы обновляем пользовательский интерфейс в главном потоке.

01
02
03
04
05
06
07
08
09
10
11
— (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == ProgressContext) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // Create Status
            NSString *status = [NSString stringWithFormat:@»Fetched %li Records», (long)[[change objectForKey:@»new»] integerValue]];
             
            // Show Progress HUD
            [SVProgressHUD setStatus:status];
        });
    }
}

Если мы хотим правильно протестировать наше приложение, нам нужно больше данных. Хотя я не рекомендую использовать следующий подход в производственном приложении, это быстрый и простой способ заполнить базу данных данными.

Откройте TSPAppDelegate.m и обновите application:didFinishLaunchingWithOptions: метод, как показано ниже. Метод populateDatabase — это простой вспомогательный метод, в котором мы добавляем фиктивные данные в базу данных.

1
2
3
4
5
6
7
8
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Populate Database
    [self populateDatabase];
     
    …
     
    return YES;
}

Реализация проста. Поскольку мы хотим вставить фиктивные данные только один раз, мы проверяем пользовательскую базу данных по умолчанию на ключ @"didPopulateDatabase" . Если ключ не установлен, мы вставляем фиктивные данные.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
— (void)populateDatabase {
    // Helpers
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    if ([ud objectForKey:@»didPopulateDatabase»]) return;
     
    for (NSInteger i = 0; i < 1000000; i++) {
        // Create Entity
        NSEntityDescription *entity = [NSEntityDescription entityForName:@»TSPItem» inManagedObjectContext:self.managedObjectContext];
         
        // Initialize Record
        NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
         
        // Populate Record
        [record setValue:[NSString stringWithFormat:@»Item %li», (long)i] forKey:@»name»];
        [record setValue:[NSDate date] forKey:@»createdAt»];
    }
     
    // Save Managed Object Context
    [self saveManagedObjectContext];
     
    // Update User Defaults
    [ud setBool:YES forKey:@»didPopulateDatabase»];
}

Количество записей важно. Если вы планируете запускать приложение на iOS Simulator, тогда можно добавить 100 000 или 1 000 000 записей. Это не будет работать так же хорошо на физическом устройстве и займет слишком много времени.

В цикле for мы создаем управляемый объект и заполняем его данными. Обратите внимание, что мы не сохраняем изменения контекста управляемого объекта во время каждой итерации цикла for .

Наконец, мы обновляем базу данных пользователя по умолчанию, чтобы убедиться, что база данных не будет заполнена при следующем запуске приложения.

Отлично. Запустите приложение в iOS Simulator, чтобы увидеть результат. Вы заметите, что асинхронному запросу на выбор требуется несколько секунд, чтобы начать выборку записей и обновить HUD прогресса.

Заменив класс контроллера извлеченных результатов асинхронным запросом выборки, мы разбили несколько частей приложения. Например, нажатие на галочку элемента списка дел больше не работает. Пока база данных обновляется, пользовательский интерфейс не отражает изменения. Решение довольно легко исправить, и я оставлю это на ваше усмотрение, чтобы реализовать решение. Теперь у вас должно быть достаточно знаний, чтобы понять проблему и найти подходящее решение.

Я уверен, что вы согласны с тем, что асинхронная выборка удивительно проста в использовании. Основные данные выполняются Core Data, что означает, что нет необходимости вручную объединять результаты асинхронного запроса выборки с контекстом управляемого объекта. Ваша единственная задача — обновить пользовательский интерфейс, когда асинхронный запрос на выборку передает вам свои результаты. Вместе с пакетными обновлениями это отличное дополнение к платформе Core Data.

Эта статья также завершает эту серию основных данных. Вы многое узнали о платформе Core Data и знаете все необходимое для использования Core Data в реальном приложении. Core Data — это мощная платформа, и с выпуском iOS 8 Apple показала нам, что с каждым годом она становится лучше.