В предыдущих статьях мы обсуждали пакетные обновления и пакетные удаления. В этом руководстве мы подробнее рассмотрим, как реализовать асинхронную выборку и в каких ситуациях ваше приложение может извлечь выгоду из этого нового API.
1. Проблема
Как и пакетные обновления, асинхронная выборка уже давно входит в список пожеланий многих разработчиков. Запрос на выборку может быть сложным, и на его выполнение уходит нетривиальное количество времени. В течение этого времени запрос на выборку блокирует поток, в котором он работает, и в результате блокирует доступ к контексту управляемого объекта, выполняющего запрос на выборку. Проблема проста для понимания, но как выглядит решение Apple.
2. Решение
Ответ Apple на эту проблему — асинхронная загрузка. Асинхронный запрос на выборку выполняется в фоновом режиме. Это означает, что он не блокирует другие задачи во время выполнения, такие как обновление пользовательского интерфейса в главном потоке.
Асинхронная выборка также имеет две другие удобные функции: отчеты о прогрессе и отмену. Асинхронный запрос на выборку может быть отменен в любое время, например, когда пользователь решает, что запрос на выборку занимает слишком много времени. Отчеты о ходе выполнения — полезное дополнение, показывающее пользователю текущее состояние запроса на выборку.
Асинхронная выборка — это гибкий API. Мало того, что можно отменить запрос асинхронной выборки, но также можно вносить изменения в контекст управляемого объекта во время выполнения запроса асинхронной выборки. Другими словами, пользователь может продолжать использовать ваше приложение, пока приложение выполняет асинхронный запрос на выборку в фоновом режиме.
3. Как это работает?
Как и пакетные обновления, асинхронные запросы на выборку передаются в контекст управляемого объекта как объект NSPersistentStoreRequest
, NSPersistentStoreRequest
экземпляр класса NSAsynchronousFetchRequest
.
Экземпляр NSAsynchronousFetchRequest
инициализируется объектом NSFetchRequest
и блоком завершения. Блок завершения выполняется, когда асинхронный запрос на выборку завершил свой запрос на выборку.
Давайте вернемся к приложению, которое мы создали ранее в этой серии, и заменим текущую реализацию класса NSFetchedResultsController
асинхронным запросом выборки.
Шаг 1: Настройка проекта
Загрузите или клонируйте проект из GitHub и откройте его в Xcode 7. Прежде чем мы сможем начать работать с классом NSAsynchronousFetchRequest
, нам нужно внести некоторые изменения. Мы не сможем использовать класс NSFetchedResultsController
для управления данными табличного представления, поскольку класс NSFetchedResultsController
был разработан для работы в основном потоке.
Шаг 2. Замена контроллера полученных результатов
Начните с обновления класса ViewController
как показано ниже. Мы удаляем свойство fetchedResultsController
и создаем новое свойство items
типа [Item]
для хранения items
fetchedResultsController
дел. Это также означает, что класс ViewController
больше не должен соответствовать протоколу NSFetchedResultsControllerDelegate
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
import UIKit
import CoreData
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let ReuseIdentifierToDoCell = «ToDoCell»
@IBOutlet weak var tableView: UITableView!
var managedObjectContext: NSManagedObjectContext!
var items: [NSManagedObject] = []
…
}
|
Прежде чем мы проведем рефакторинг viewDidLoad()
, я сначала хочу обновить реализацию протокола UITableViewDataSource
. Посмотрите на изменения, которые я сделал.
1
2
3
|
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
|
1
2
3
|
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
func configureCell(cell: ToDoCell, atIndexPath indexPath: NSIndexPath) {
// Fetch Record
let record = items[indexPath.row]
// Update Cell
if let name = record.valueForKey(«name») as?
cell.nameLabel.text = name
}
if let done = record.valueForKey(«done») as?
cell.doneButton.selected = done
}
cell.didTapButtonHandler = {
if let done = record.valueForKey(«done») as?
record.setValue(!done, forKey: «done»)
}
}
}
|
1
2
3
4
5
6
7
8
9
|
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if (editingStyle == .Delete) {
// Fetch Record
let record = items[indexPath.row]
// Delete Record
managedObjectContext.deleteObject(record)
}
}
|
Нам также нужно изменить одну строку кода в prepareForSegue(_:sender:)
как показано ниже.
1
2
|
// Fetch Record
let record = items[indexPath.row]
|
И последнее, но не менее важное: удалите реализацию протокола NSFetchedResultsControllerDelegate
, поскольку он нам больше не нужен.
Шаг 3: Создание асинхронного запроса на выборку
Как вы можете видеть ниже, мы создаем асинхронный запрос на выборку в 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
|
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: «Item»)
// Add Sort Descriptors
fetchRequest.sortDescriptors = [NSSortDescriptor(key: «createdAt», ascending: true)]
// Initialize Asynchronous Fetch Request
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.processAsynchronousFetchResult(asynchronousFetchResult)
})
}
do {
// Execute Asynchronous Fetch Request
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest)
print(asynchronousFetchResult)
} catch {
let fetchError = error as NSError
print(«\(fetchError), \(fetchError.userInfo)»)
}
}
|
Мы начнем с создания и настройки экземпляра NSFetchRequest
для инициализации асинхронного запроса на выборку. Именно этот запрос выборки будет выполняться в фоновом режиме асинхронным запросом выборки.
1
2
3
4
5
|
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: «Item»)
// Add Sort Descriptors
fetchRequest.sortDescriptors = [NSSortDescriptor(key: «createdAt», ascending: true)]
|
Чтобы инициализировать экземпляр NSAsynchronousFetchRequest
, мы вызываем init(request:completionBlock:)
, передавая fetchRequest
и блок завершения.
1
2
3
4
5
6
|
// Initialize Asynchronous Fetch Request
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.processAsynchronousFetchResult(asynchronousFetchResult)
})
}
|
Блок завершения вызывается, когда асинхронный запрос на выборку завершил выполнение своего запроса на выборку. Блок завершения принимает один аргумент типа NSAsynchronousFetchResult
, который содержит результат запроса, а также ссылку на исходный запрос асинхронной выборки.
В блоке завершения мы вызываем processAsynchronousFetchResult(_:)
, передавая объект NSAsynchronousFetchResult
. Мы рассмотрим этот вспомогательный метод через несколько минут.
Выполнение асинхронного запроса на выборку практически идентично тому, как мы выполняем NSBatchUpdateRequest
. Мы вызываем executeRequest(_:)
в контексте управляемого объекта, передавая асинхронный запрос на выборку.
01
02
03
04
05
06
07
08
09
10
|
do {
// Execute Asynchronous Fetch Request
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest)
print(asynchronousFetchResult)
} catch {
let fetchError = error as NSError
print(«\(fetchError), \(fetchError.userInfo)»)
}
|
Несмотря на то, что асинхронный запрос на выборку выполняется в фоновом режиме, обратите внимание, что метод executeRequest(_:)
возвращается немедленно, передавая нам объект NSAsynchronousFetchResult
. Как только асинхронный запрос на выборку завершается, тот же объект NSAsynchronousFetchResult
заполняется результатом запроса на выборку.
Помните из предыдущего урока, что executeRequest(_:)
является методом метания. Мы отлавливаем любые ошибки в предложении do-catch
оператора do-catch
и выводим их на консоль для отладки.
Шаг 4: Обработка результата асинхронной выборки
Метод processAsynchronousFetchResult(_:)
— это не что иное, как вспомогательный метод, в котором мы обрабатываем результат запроса асинхронной выборки. Мы устанавливаем свойство items
контроллера представления с содержимым свойства finalResult
результата и перезагружаем табличное представление.
1
2
3
4
5
6
7
8
9
|
func processAsynchronousFetchResult(asynchronousFetchResult: NSAsynchronousFetchResult) {
if let result = asynchronousFetchResult.finalResult {
// Update Items
items = result as!
// Reload Table View
tableView.reloadData()
}
}
|
Шаг 5: Сборка и запуск
Создайте проект и запустите приложение в iOS Simulator. Если ваше приложение дает сбой, когда оно пытается выполнить асинхронный запрос на выборку, то вы можете использовать API, который устарел с iOS 9 (и OS X El Capitan).
В Core Data и Swift: Concurrency я объяснил различные типы параллелизма, которые может иметь контекст управляемого объекта. Начиная с iOS 9 (и OS X El Capitan), ConfinementConcurrencyType
устарел. То же самое верно для метода init()
класса NSManagedObjectContext
, потому что он создает экземпляр с типом параллелизма ConfinementConcurrencyType
.
Если ваше приложение дает сбой, вы, скорее всего, используете контекст управляемого объекта с типом параллелизма ConfinementConcurrencyType
, который не поддерживает асинхронную выборку. К счастью, решение простое. Создайте контекст управляемого объекта, используя указанный инициализатор init(concurrencyType:)
, передавая MainQueueConcurrencyType
или PrivateQueueConcurrencyType
в качестве типа параллелизма.
4. Показ прогресса
Класс NSAsynchronousFetchRequest
добавляет поддержку для отслеживания хода выполнения запроса на выборку, и даже можно отменить асинхронный запрос на выборку, например, если пользователь решит, что его выполнение занимает слишком много времени.
Класс NSAsynchronousFetchRequest
использует класс NSProgress
для отчетов о ходе выполнения, а также для отмены асинхронного запроса на выборку. Класс NSProgress
, доступный начиная с iOS 7 и OS X Mavericks, — это умный способ отслеживать ход выполнения задачи без необходимости тесно связывать задачу с пользовательским интерфейсом.
Класс NSProgress
также поддерживает отмену, что позволяет отменить запрос асинхронной выборки. Давайте выясним, что нам нужно сделать для реализации отчетов о ходе выполнения асинхронного запроса на выборку.
Шаг 1: Добавление SVProgressHUD
Мы покажем пользователю ход выполнения асинхронного запроса на выборку с помощью библиотеки SVProgressHUD Сэма Верметта . Самый простой способ сделать это — через CocoaPods . Так выглядит проектный подфайл .
1
2
3
4
5
|
source ‘https://github.com/CocoaPods/Specs.git’
platform :ios, ‘9.0’
use_frameworks!
pod ‘SVProgressHUD’, ‘~> 1.1’
|
Запустите pod install
из командной строки и не забудьте открыть рабочее пространство, созданное CocoaPods вместо проекта Xcode.
Шаг 2: Настройка NSProgress
В этой статье мы не NSProgress
подробно изучать класс NSProgress
, но не стесняйтесь читать больше об этом в документации Apple . Мы создаем экземпляр NSProgress
в viewDidLoad()
контроллера представления перед тем, как выполнить запрос асинхронной выборки.
1
2
3
4
5
|
// Create Progress
let progress = NSProgress(totalUnitCount: 1)
// Become Current
progress.becomeCurrentWithPendingUnitCount(1)
|
Вы можете быть удивлены тем, что мы установили общее количество единиц на 1
. Причина проста. Когда Core Data выполняет запрос асинхронной выборки, он не знает, сколько записей он найдет в постоянном хранилище. Это также означает, что мы не сможем показать относительный прогресс пользователю — процент. Вместо этого мы покажем пользователю абсолютный прогресс — количество найденных им записей.
Вы можете устранить эту проблему, выполнив запрос на выборку, чтобы получить количество записей перед выполнением асинхронного запроса на выборку. Я предпочитаю этого не делать, потому что это также означает, что выборка записей из постоянного хранилища занимает больше времени из-за дополнительного запроса на выборку в начале.
Шаг 3: Добавление наблюдателя
Когда мы выполняем асинхронный запрос на выборку, нам немедленно передается объект NSAsynchronousFetchResult
. Этот объект имеет свойство progress
, которое имеет тип NSProgress
. Именно это свойство progress
мы должны наблюдать, если хотим получать обновления прогресса.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// Create Progress
let progress = NSProgress(totalUnitCount: 1)
// Become Current
progress.becomeCurrentWithPendingUnitCount(1)
// Execute Asynchronous Fetch Request
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest) as!
if let asynchronousFetchProgress = asynchronousFetchResult.progress {
asynchronousFetchProgress.addObserver(self, forKeyPath: «completedUnitCount», options: NSKeyValueObservingOptions.New, context: nil)
}
// Resign Current
progress.resignCurrent()
|
Обратите внимание, что мы вызываем resignCurrent
для объекта progress
чтобы сбалансировать предыдущий becomeCurrentWithPendingUnitCount:
Имейте в виду, что оба эти метода должны быть вызваны в одном потоке.
Шаг 4. Удаление наблюдателя
В блоке завершения асинхронного запроса на выборку мы удаляем наблюдателя и закрываем HUD прогресса.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// Initialize Asynchronous Fetch Request
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Dismiss Progress HUD
SVProgressHUD.dismiss()
// Process Asynchronous Fetch Result
self.processAsynchronousFetchResult(asynchronousFetchResult)
if let asynchronousFetchProgress = asynchronousFetchResult.progress {
// Remove Observer
asynchronousFetchProgress.removeObserver(self, forKeyPath: «completedUnitCount»)
}
})
}
|
Прежде чем мы реализуем observeValueForKeyPath(_:ofObject:change:context:)
, нам нужно показать прогресс HUD перед созданием асинхронного запроса на выборку.
1
2
|
// Show Progress HUD
SVProgressHUD.showWithStatus(«Fetching Data», maskType: .Gradient)
|
Шаг 5: Отчет о проделанной работе
Все, что нам осталось сделать, — это реализовать метод observeValueForKeyPath(_:ofObject:change:context:)
. Мы проверяем, равен ли context
ProgressContext
, создаем объект status
, извлекая количество выполненных записей из словаря change
, и обновляем HUD прогресса. Обратите внимание, что мы обновляем пользовательский интерфейс в главном потоке.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == «completedUnitCount» {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let changes = change, number = changes[«new»] {
// Create Status
let status = «Fetched \(number) Records»
// Show Progress HUD
SVProgressHUD.setStatus(status)
}
})
}
}
|
5. Фиктивные данные
Если мы хотим правильно протестировать наше приложение, нам нужно больше данных. Хотя я не рекомендую использовать следующий подход в производственном приложении, это быстрый и простой способ заполнить базу данных данными.
Откройте AppDelegate.swift и обновите application(_:didFinishLaunchingWithOptions:)
метод application(_:didFinishLaunchingWithOptions:)
как показано ниже. Метод populateDatabase()
— это простой вспомогательный метод, в котором мы добавляем фиктивные данные в базу данных.
1
2
3
4
5
6
7
8
|
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Populate Database
populateDatabase()
…
return true
}
|
Реализация проста. Поскольку мы хотим вставить фиктивные данные только один раз, мы проверяем пользовательскую базу данных по умолчанию на наличие ключа "didPopulateDatabase"
. Если ключ не установлен, мы вставляем фиктивные данные.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private func populateDatabase() {
// Helpers
let userDefaults = NSUserDefaults.standardUserDefaults()
guard userDefaults.objectForKey(«didPopulateDatabase») == nil else { return }
// Create Entity
let entityDescription = NSEntityDescription.entityForName(«Item», inManagedObjectContext: self.managedObjectContext)
for index in 0…1000000 {
// Initialize Record
let record = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
// Populate Record
record.setValue(NSDate(), forKey: «createdAt»)
record.setValue(«Item \(index)», forKey: «name»)
}
// Save Changes
saveManagedObjectContext()
// Update User Defaults
userDefaults.setBool(true, forKey: «didPopulateDatabase»)
}
|
Количество записей важно. Если вы планируете запускать приложение на iOS Simulator, тогда можно добавить 100 000 или 1 000 000 записей. Это не будет работать так же хорошо на физическом устройстве и займет слишком много времени.
В цикле for
мы создаем управляемый объект и заполняем его данными. Обратите внимание, что мы не сохраняем изменения контекста управляемого объекта во время каждой итерации цикла for
.
Наконец, мы обновляем базу данных пользователя по умолчанию, чтобы убедиться, что база данных не будет заполнена при следующем запуске приложения.
Отлично. Запустите приложение в iOS Simulator, чтобы увидеть результат. Вы заметите, что асинхронному запросу на выбор требуется несколько секунд, чтобы начать выборку записей и обновить HUD прогресса.
6. Срочные Изменения
Заменив класс контроллера извлеченных результатов асинхронным запросом выборки, мы разбили несколько частей приложения. Например, нажатие на галочку элемента списка дел больше не работает. Пока база данных обновляется, пользовательский интерфейс не отражает изменения. Решение довольно легко исправить, и я оставлю это на ваше усмотрение, чтобы реализовать решение. Теперь у вас должно быть достаточно знаний, чтобы понять проблему и найти подходящее решение.
Вывод
Я уверен, что вы согласны с тем, что асинхронная выборка удивительно проста в использовании. Основные данные выполняются Core Data, что означает, что нет необходимости вручную объединять результаты асинхронного запроса выборки с контекстом управляемого объекта. Ваша единственная задача — обновить пользовательский интерфейс, когда асинхронный запрос на выборку выдаст вам результаты.