Что такое continuation в Swift Concurrency
Что проверяют
Умеете ли вы связать старый мир обратных вызовов с async/await и понимаете ли, что происходит с задачей в момент приостановки. Для iOS-разработчика это ежедневная работа: делегаты, замыкания и чужие SDK не переписываются за один день, их заворачивают в асинхронные функции.
Как отвечать
Continuation это объект, который держит точку приостановки асинхронной функции. Вы получаете его в withCheckedContinuation или withCheckedThrowingContinuation, отдаёте в колбэк, и функция стоит на await, пока кто-нибудь не вызовет resume. После этого задача продолжается с переданным значением или выбрасывает ошибку.
func loadImage(id: String) async throws -> UIImage {
try await withCheckedThrowingContinuation { continuation in
legacyLoader.load(id: id) { image, error in
if let image {
continuation.resume(returning: image)
} else {
continuation.resume(throwing: error ?? LoaderError.unknown)
}
}
}
}Главное правило: resume вызывается ровно один раз, ни нуля, ни двух. Проверяемый вариант ловит нарушение в рантайме, небезопасный не проверяет ничего и стоит дешевле, но брать его имеет смысл только после замеров.
Отмену стоит упомянуть отдельно: сам по себе continuation её не поддерживает, для этого есть withTaskCancellationHandler, где по отмене вы дёргаете отмену у старого API.
Где спотыкаются
Забывают ветку, где колбэк вернул и пустые данные, и пустую ошибку. Задача повисает навсегда, а вместе с ней экран с индикатором. Проверяйте, что из каждого пути ведёт ровно один resume.
Разворачивают опционал восклицательным знаком, копируя пример из статьи. Старый API почти всегда отдаёт опционалы, и падение случится не в тесте, а у пользователя в метро без сети.
Заворачивают в continuation то, что уже имеет асинхронный интерфейс. У сетевых и файловых API современные версии SDK дают методы с async сами, и ручная обёртка добавляет только шанс ошибиться с resume.