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))
    }
}

Здесь у нас есть:

  1. DataType Перечисление для различных форматов данных.
  2. type Свойство со значением по умолчанию , чтобы избежать дополнительных значений.
  3. webView Свойство для загрузки данных.
  4. createData Функция для обработки dataTypecompletionHandler и загрузка веб – данных для прошедшего 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, мы будем использовать withCheckedThrowingContinuationfunction.

Напишем функцию для асинхронной загрузки веб-контента по 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 – это глобальный актор, который позволяет нам выполнять код в основной очереди. Все UIViews (следовательно 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 с вашими любимыми учебными материалами по этой теме!

Ответить