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.

Подготовка

Давайте пробежимся по имплементации 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 и слабые ссылки на self для избежания циклов удержания. Можем ли мы усовершенствовать этот код c async/await? Давайте попробуем!

Добавление асинхронного кода

Мы начинаем рефакторить функцию createData в асинхронном стиле:

func createData(url: URL, type: DataType) async throws -> Data

Перед тем, как начать работу с веб-контентом, мы должны убедиться, что навигация в webview завершена. Мы можем обработать ее в функции webView(_:didFinish:) у WKNavigationDelegate. Мы будем использовать функцию withCheckedThrowingContinuation, чтобы сделать эту логику совместимой с async\await.
Давайте напишем функцию для асинхронной загрузки веб-контента через 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)     } } 

Но если вы запустите этот код, вы получите ошибку:
Call to main actor-isolated instance method ‘load’ in a synchronous nonisolated context

Мы добавляем атрибут MainActor, чтобы починить это:

@MainActor private func load(_ url: URL) async throws {   // implementation }

MainActor – это глобальный актор, позволяющий выполнять код в основной очереди. Все UIView (а значит, и WKWebView) объявляются с этим атрибутом и используются в основной очереди.

Теперь мы можем вызвать функцию load:

@MainActor func createData(url: URL, type: DataType) async throws -> Data {     try await load(url)     // To be implemented     return Data() }

Мы помечаем функцию createData с помощью атрибута MainActor, потому что функция load должна вызываться в основной очереди. Более того, мы можем добавить этот атрибут в класс 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)

Оно также работает для генерации картинки, однако создание web-архива по-прежнему доступно только с коллбеком. Здесь пригодится функция withCheckedThrowingContinuation:

import WebKit  extension WKWebView {      func webArchiveData() async throws -> Data {         try await withCheckedThrowingContinuation { continuation in             createWebArchiveData { result in                 continuation.resume(with: result)             }         }     } }

Обратите внимание, что continuation может автоматически обрабатывать значения 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()     } }

Мы обрабатываем все ошибки в одном месте и уменьшаем места захвата self в замыканиях.

Использование новых асинхронных функций

Ура, мы сделали это! Погодите, но как использовать новые асинхронные функции из синхронного контекста? С созданием объекта 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.

Заключение

На первый взгляд новая модель многопоточности выглядит как синтаксический сахар. Однако, его использование приводит к более безопасному и структурированному коду. Мы легко можем избежать захвата self в замыканиях и улучшить обработку ошибок. Я продолжаю «играть» с async/await и собирать полезные ресурсы в репозитории awesome-swift-async-await. Буду рад, если вы поделитесь своими любимыми материалами по этой теме!


ссылка на оригинал статьи https://habr.com/ru/company/skyeng/blog/598295/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *