Статьи

Основные данные и Swift: асинхронная выборка

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

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

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

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

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

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

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

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

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

Начните с обновления класса 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 , поскольку он нам больше не нужен.

Как вы можете видеть ниже, мы создаем асинхронный запрос на выборку в 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 и выводим их на консоль для отладки.

Метод 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()
    }
}

Создайте проект и запустите приложение в 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 в качестве типа параллелизма.

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

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

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

Мы покажем пользователю ход выполнения асинхронного запроса на выборку с помощью библиотеки 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.

В этой статье мы не 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 выполняет запрос асинхронной выборки, он не знает, сколько записей он найдет в постоянном хранилище. Это также означает, что мы не сможем показать относительный прогресс пользователю — процент. Вместо этого мы покажем пользователю абсолютный прогресс — количество найденных им записей.

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

Когда мы выполняем асинхронный запрос на выборку, нам немедленно передается объект 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: Имейте в виду, что оба эти метода должны быть вызваны в одном потоке.

В блоке завершения асинхронного запроса на выборку мы удаляем наблюдателя и закрываем 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)

Все, что нам осталось сделать, — это реализовать метод 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)
            }
        })
    }
}

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

Откройте 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 прогресса.

Отображение хода выполнения запроса асинхронной выборки

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

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