Async / await для существующих приложений iOS
Ранее я писал пост о работе с контентом веб-просмотра в автономном режиме . С тех пор команда Apple выпустила бета-версию Xcode 13.2 с Swift 5.5, я прочитал книгу о современной модели параллелизма в Swift , и я думаю, что сейчас идеальное время для обновления моих примеров с помощью async / await!
Перед чтением этого поста я настоятельно рекомендую проверить статью о параллелизме в Swift Language Guide.
Примечание. Примеры кода написаны на Swift 5.5 и протестированы на iOS 15.0 с бета-версией Xcode 13.2 (13C5081f).
Подготовка
Давайте освежим в памяти реализацию того, WebDataManager
что позволяет нам получать данные для веб-контента по URL-адресу:
import WebKit
final class WebDataManager: NSObject {
enum DataError: Error {
case noImageData
}
// 1
enum DataType: String, CaseIterable {
case snapshot = "Snapshot"
case pdf = "PDF"
case webArchive = "Web Archive"
}
// 2
private var type: DataType = .webArchive
// 3
private lazy var webView: WKWebView = {
let webView = WKWebView()
webView.navigationDelegate = self
return webView
}()
private var completionHandler: ((Result<Data, Error>) -> Void)?
// 4
func createData(url: URL, type: DataType, completionHandler: @escaping (Result<Data, Error>) -> Void) {
self.type = type
self.completionHandler = completionHandler
webView.load(.init(url: url))
}
}
Здесь у нас есть:
DataType
Перечисление для различных форматов данных.type
Свойство со значением по умолчанию , чтобы избежать дополнительных значений.webView
Свойство для загрузки данных.createData
Функция для обработкиdataType
,completionHandler
и загрузка веб – данных для прошедшего URL.
Чего здесь не хватает? Конечно, WKNavigationDelegate
реализация:
extension WebDataManager: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
switch type {
case .snapshot:
let config = WKSnapshotConfiguration()
config.rect = .init(origin: .zero, size: webView.scrollView.contentSize)
webView.takeSnapshot(with: config) { [weak self] image, error in
if let error = error {
self?.completionHandler?(.failure(error))
return
}
guard let pngData = image?.pngData() else {
self?.completionHandler?(.failure(DataError.noImageData))
return
}
self?.completionHandler?(.success(pngData))
}
case .pdf:
let config = WKPDFConfiguration()
config.rect = .init(origin: .zero, size: webView.scrollView.contentSize)
webView.createPDF(configuration: config) { [weak self] result in
self?.completionHandler?(result)
}
case .webArchive:
webView.createWebArchiveData { [weak self] result in
self?.completionHandler?(result)
}
}
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
completionHandler?(.failure(error))
}
}
Здесь у нас есть 6 призывов completionHandler
и слабое использование себя, чтобы избежать циклов сохранения. Можем ли мы улучшить этот код с помощью async / awaits? Давайте попробуем!
Добавление асинхронного кода
Начнем с createData
асинхронного рефакторинга :
func createData(url: URL, type: DataType) async throws -> Data
Прежде чем работать с содержимым веб-представления, мы должны быть уверены, что навигация по главному фрейму завершена. Мы можем справиться с этим в webView(_:didFinish:)
функции WKNavigationDelegate
. Чтобы сделать эту логику совместимой с async \ await, мы будем использовать withCheckedThrowingContinuation
function.
Напишем функцию для асинхронной загрузки веб-контента по URL:
private var continuation: CheckedContinuation<Void, Error>?
private func load(_ url: URL) async throws {
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
self.webView.load(.init(url: url))
}
}
Мы храним его continuation
для использования в функциях делегата. Для обработки обновлений навигации мы добавляем continuation
использование:
extension WebDataManager: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
continuation?.resume(returning: ())
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
continuation?.resume(throwing: error)
}
}
Но если вы попытаетесь запустить этот код, вы получите ошибку:
Вызов метода экземпляра, изолированного от основного субъекта, load в синхронном неизолированном контексте
Чтобы исправить это, мы добавляем MainActor
атрибут:
@MainActor
private func load(_ url: URL) async throws {
// implementation
}
MainActor – это глобальный актор, который позволяет нам выполнять код в основной очереди. Все UIView
s (следовательно WKWebView
) объявляются с этим атрибутом и доступны в основной очереди.
Теперь мы можем вызвать load
функцию:
@MainActor
func createData(url: URL, type: DataType) async throws -> Data {
try await load(url)
// To be implemented
return Data()
}
Поскольку load
функция должна вызываться в основной очереди, мы также помечаем createData
функцию MainActor
атрибутом. Более того, мы можем добавить этот атрибут в WebDataManager
класс вместо всех функций:
@MainActor
final class WebDataManager: NSObject {
// implementation
}
Работа с системными API async / await
Теперь мы готовы переписать создание данных веб-контента. Вот старый пример создания PDF:
let config = WKPDFConfiguration()
config.rect = .init(origin: .zero, size: webView.scrollView.contentSize)
webView.createPDF(configuration: config) { [weak self] result in
self?.completionHandler?(result)
}
К счастью, команда Apple добавила аналоги async / await для множества существующих функций с обратными вызовами:
let config = WKPDFConfiguration()
config.rect = .init(origin: .zero, size: webView.scrollView.contentSize)
return try await webView.pdf(configuration: config)
Он также работает для создания снимков изображений, но создание веб-архива по-прежнему доступно только с обработчиками завершения. Вот хороший шанс для другой withCheckedThrowingContinuation
функции:
import WebKit
extension WKWebView {
func webArchiveData() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
createWebArchiveData { result in
continuation.resume(with: result)
}
}
}
}
Обратите внимание, что продолжение может автоматически обрабатывать состояние данного Result
значения и связанных с ним значений.
Окончательная версия createData
функции выглядит лучше:
func createData(url: URL, type: DataType) async throws -> Data {
try await load(url)
switch type {
case .snapshot:
let config = WKSnapshotConfiguration()
config.rect = .init(origin: .zero, size: webView.scrollView.contentSize)
let image = try await webView.takeSnapshot(configuration: config)
guard let pngData = image.pngData() else {
throw DataError.noImageData
}
return pngData
case .pdf:
let config = WKPDFConfiguration()
config.rect = .init(origin: .zero, size: webView.scrollView.contentSize)
return try await webView.pdf(configuration: config)
case .webArchive:
return try await webView.webArchiveData()
}
}
У нас есть одно место, где можно обрабатывать все ошибки и сокращать захват себя при закрытии.
Использование новых асинхронных функций
Поздравляем, у нас получилось! Подождите, а как использовать новые асинхронные функции из контекста синхронизации? С экземплярами Task
мы можем выполнять асинхронные задачи:
Task {
do {
let url = URL(string: "https://www.artemnovichkov.com")!
let data = try await webDataManager.createData(url: url, type: .pdf)
print(data)
}
catch {
print(error)
}
}
Чтобы получить окончательный результат, проверьте проект OfflineDataAsyncExample на Github.
Заключение
На первый взгляд новая модель параллелизма выглядит как синтаксический сахар. Однако это приводит к более безопасному и более структурированному коду. Мы можем легко избежать замыканий с помощью самозахвата и улучшить обработку ошибок. Я все еще играю в async / awaits и собираю полезные ресурсы в репозитории awesome-swift-async-await . Не стесняйтесь присылать PR с вашими любимыми учебными материалами по этой теме!