
В предыдущей части мы разобрали преимущества работы с async/await по сравнению с GCD. В этой части мы более подробно рассмотрим ключевые слова async и await (и не только). Разберемся в том, как они работают, что означает "неблокирующее ожидание" и самое главное рассмотрим это все на примерах.
Статьи из серии
Swift async/await на примерах
Оглавление
Что такое swift async/await
Примеры
Async/await. Http запрос
Async computed property. Загрузка изображения
Async let. Одновременная загрузка двух изображений
AsyncSequence. Отображение процента загрузки изображения
AsyncStream. Перенос логики загрузки изображения
Итоги
Полезные ссылки
Что такое swift async/await
Swift async/await - это новая возможность, добавленная в Swift 5.5, которая расширяет функциональность языка путем введения асинхронных функций. Основная особенность асинхронных функций заключается в том, что они могут приостанавливаться без блокировки текущего потока. Вместо блокировки функция передает управление системе, которая решает, чем далее занять поток(и). Таким образом, достигается неблокирующее ожидание.
Примеры
В примерах, которые будут приведены далее, я буду использовать сервис jsonplaceholder. Он предоставляет API для тестирования. С помощью этого сервиса можно отправлять различные запросы и получать шаблонные данные.
Async/await. Http запрос
Первым делом давайте выполним обычный http запрос:
// 1
struct Photo: Decodable {
let albumId: Int
let id: Int
let title: String
let url: URL
let thumbnailUrl: URL
}
// 2
func getPhotos(by albumId: Int) async throws -> [Photo] {
// 3
let url = URL(string: "https://jsonplaceholder.typicode.com/albums/\(albumId)/photos")!
let request = URLRequest(url: url)
// 4
let (data, _) = try await URLSession.shared.data(for: request)
// 5
let photos = try JSONDecoder().decode([Photo].self, from: data)
return photos
}
Создаем decodable структуру, в которую мы будем преобразовывать полученный JSON.
Создаем функцию, которая будет запрашивать массив объектов типа Photo по id альбома с сервера. Помечаем нашу функцию ключевым словом
async
. Этим мы сообщаем компилятору, что наша функция потенциально может иметь suspension points (точки приостановки). Грубо говоря, пометив функцию какasync
, мы можем вызывать внутри этой функции другиеasync
функции с помощьюawait
. Также помечаем нашу функцию ключевым словомthrows
, что позволит языковыми конструкциямиdo/try/catch
обрабатывать ошибки, вызывая нашу функцию.Формируем GET запрос к серверу.
Отправляем наш запрос с помощью нового асинхронного метода URLSession.data. Чтобы вызвать асинхронную функцию и получить результат в том же месте, воспользуемся ключевым словом
await
. Метод возвращает кортеж(Data, URLResponse)
. В идеале, стоило бы проверитьresponse
на наличие ошибок, но для компактности и простоты я пропущу этот шаг.Преобразуем полученный JSON в массив структур и вернем полученный результат из функции.
Остановимся чуть подробней на пунктах 2 и 4. Вызывая async
функцию у URLSession
, мы приостанавливаем выполнение нашей функции. Во время приостановки мы не блокируем текущий поток. Вместо этого система нагружает его другими полезными задачами. Система продолжит выполнение getPhotos
, когда метод у URLSession
завершит свое выполнение. Функция getPhotos
не может быть синхронной, так как она должна иметь возможность приостанавливаться и возобновляться. Из-за этого требуется помечать такие функции как async
. Благодаря этому ключевому слову компилятор знает, что функция работает с асинхронным контекстом.
Можно провести аналогию с throws
функциями. Если наша функция может выбросить ошибку, то ее нужно пометить как throws
. Вызывать throws
функции можно только с помощью try
. Это строгие правила языка. Такие же и у ключевых слов async/await
.
С помощью ключевого слова await
мы (и компилятор в нашем лице) разделяем нашу функцию на части (partials). Функция может быть приостановлена между этими partials. Сами по себе они выполняются без прерываний. Система сама планирует и выполняет эти части в определенном порядке.
func getPhotos(by albumId: Int) async throws -> [Photo] {
let url = URL(string: "https://jsonplaceholder.typicode.com/albums/\(albumId)/photos")!
let request = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(for: request)
// --------------
let photos = try JSONDecoder().decode([Photo].self, from: data)
return photos
}
Хоть визуально код до и после await располагается на соседних строчках, в действительности код после await
может быть вызван через длительное время, особенно при работе с сетевыми запросами. Кроме того, он может продолжить выполнение вообще на другом потоке. Об этом важно помнить при работе с UI.

Async computed property. Загрузка изображения
Ключевым словом async можно помечать так же замыкания, инициализаторы и вычисляемые свойства (computed property). В этом примере мы воспользуемся асинхронным вычисляемым свойством. На его основе реализуем простой класс для загрузки изображений.
// 5
enum ImageLoaderError: Error {
case incorrectImageData
}
// 6
final class ImageLoader: Sendable {
private let imageUrl: URL
// 1
init(imageUrl: URL) {
self.imageUrl = imageUrl
}
// 2
var image: UIImage {
// 3
get async throws {
// 4
let (data, _) = try await URLSession.shared.data(from: imageUrl)
// 5
guard let image = UIImage(data: data) else {
throw ImageLoaderError.incorrectImageData
}
return image
}
}
}
В инициализаторе ожидаем URL по которому в дальнейшем будем загружать изображение.
Изображение будем загружать через вызов computed property.
Помечаем геттер ключевым словом
async
. Это позволит вызывать внутри другиеasync
функции. Еще одно новшество, не относящееся к async/await, заключается в том, что геттер теперь может быть throws (выкидывать ошибки).Вызываем уже знакомый нам метод у URLSession. Только теперь передаем ему не
URLRequest
, а просто URL.UIImage(data:)
может завершиться с ошибкой. В таком случае инициализатор вернет nil. Для насnil
- это лишнее состояние, ведь оно эквивалентно ошибочному. Будем возвращатьImageLoaderError
вместоnil
. В таком случае клиентам (пользователям класса) будет проще обрабатывать ошибки (и не обрабатывать отдельно опционал).Подписываем наш класс под протокол Sendable. Этим мы говорим что наш объект можно безопасно передавать между async контекстами. Это необходимо с версии Swift 6.0 (иначе будет возникать ряд ошибок при работе с этим классом из async контекста). Подробнее остановимся на этом протоколе в следующих частях.
Стоит так же отметить, что сеттер не может быть async
, это работает только с геттером. При асинхронном геттере компилятор не даст создать даже обычный (не асинхронный) сеттер. Аналогичная ситуация возникает и с ключевым словом throws
. Если пометить им геттер, то сеттер создать вообще не получится.

Давайте напишем небольшой контроллер и воспользуемся функцией из нашего примера для загрузки данных для изображений и нашим новым объектом чтоб загрузить изображение.
class ViewController: UIViewController {
private let imageView: UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
// 1
func getPhotos(by albumId: Int) async throws -> [Photo] {
let url = URL(string: "https://jsonplaceholder.typicode.com/albums/\(albumId)/photos")!
let request = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(for: request)
let photos = try JSONDecoder().decode([Photo].self, from: data)
return photos
}
override func viewDidLoad() {
super.viewDidLoad()
fillView()
// 2
Task {
do {
// 3
let photos = try await getPhotos(by: 1)
// 4
let imageLoader = ImageLoader(imageUrl: photos[0].url)
imageView.image = try await imageLoader.image
} catch {
// 5
print(error.localizedDescription)
}
}
}
private func fillView() {
view.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
imageView.heightAnchor.constraint(equalToConstant: 248),
imageView.widthAnchor.constraint(equalToConstant: 248),
])
}
}
Функция из первого примера
Метод viewDidLoad - синхронный. Мы не можем использовать
await
для ожидания выполнения асинхронных функций внутри синхронного метода. Для этого нужно использовать новую сущность -Task
. Мы поговорим про нее подробнее в следующих частях. Сейчас же стоит знать, что если мы хотим вызватьasync
функцию из синхронной, то вызов нужно осуществлять внутриTask
.Получаем массив данных для фотографий с помощью нашего метода.
С помощью новой сущности и его async computed property
image
загружаем изображение и в этой же строчке добавляем его нашейimageView
.С помощью конструкции
do/try/catch
мы можем обработать сразу несколькоthrows
функций внутри одного блока. Если обработка ошибок не предусмотрена, то внутриTask
можно не использоватьdo/try/catch
, так как замыкание которое мы передаем вTask
может выбрасывать ошибки (помечено ключевым словомthrows
).
Внутри нашей таски мы вызываем асинхронный метод getPhotos
и с помощью await
ждем результата. Когда функция вернет массив [Photo]
, выполнение продолжится. После мы достаем URL первого изображения и создаем на его основе объект ImageLoader
. Далее так же ожидаем выполнения его асинхронного вычисляемого значения image
и в заключении присваиваем полученное изображение в imageView.image
.

Не забываем, что getPhotos
и ImageLoader
внутри себя так же вызывают асинхронные функции и приостанавливают свое выполнение ожидая результатов.
Async let. Одновременная загрузка двух изображений.
Немного видоизменим предыдущий пример. Будем загружать 2 разных изображения и рендерить из них одно. Поменяем код внутри Task
Task {
// 1
let photos = try await getPhotos(by: 1)
// 2
let firstImage = try await ImageLoader(imageUrl: photos[0].url).image
let secondImage = try await ImageLoader(imageUrl: photos[1].url).image
// 3
let size = firstImage.size
let mergedImage = UIGraphicsImageRenderer(size: size).image { ctx in
let rect = CGRect(origin: .zero, size: size)
firstImage.draw(in: rect)
secondImage.draw(in: rect, blendMode: .normal, alpha: 0.5)
}
// 4
imageView.image = mergedImage
}
Как и в предыдущем примере, сначала получаем массив данных изображений.
Загружаем поочередно два изображения. Для этого просто беру url у первых двух элементов из массива.
Рендерим из них одно изображение.
Присваиваем полученный результат в
imageView
.
Все отлично работает, но есть один момент. Мы начинаем грузить первое изображение и ждем, пока оно загрузится с помощью await
. Только после того, как наше первое изображение загрузилось, мы начинаем загружать второе.

Можно ли загружать оба изображения одновременно? Как вы наверное уже догадались - да. Один из способов - воспользоваться новой конструкцией async let. Давайте поправим наш пример.
Task {
let photos = try await getPhotos(by: 1)
// 1
async let firstImageTask = ImageLoader(imageUrl: photos[0].url).image
async let secondImageTask = ImageLoader(imageUrl: photos[1].url).image
// 2
let firstImage = try await firstImageTask
let secondImage = try await secondImageTask
let size = firstImage.size
let mergedImage = UIGraphicsImageRenderer(size: size).image { ctx in
let rect = CGRect(origin: .zero, size: size)
firstImage.draw(in: rect)
secondImage.draw(in: rect, blendMode: .normal, alpha: 0.5)
}
imageView.image = mergedImage
}
Теперь мы не await'им изображения, а с помощью
async let
создаем дочерние таски (про дочерние таски поговорим подробнее в одной из следующих частей). Дочерние задачи сразу отправляются на планирование в систему, и система запускает их при первой возможности. Наша функция не прерывается наasync let
, выполнение идет дальше после этой конструкции. Вызов асинхронной функции с помощью async let не является потенциальной точкой приостановки (suspension point).Ожидаем завершения задач, запущенных на первом шаге. В этом случае мы можем использовать
await
для ожидания их поочередно, так как вторая задача продолжает выполнение, пока мы ожидаем завершения первой.
Таска, созданная с помощью async let
, может завершиться до того, как мы решим получить из нее значение. В таком случае, при вызове await
, мы сразу же получим его.

Также здесь стоит упомянуть, что после ключевого слова await
в выражении можно указывать любое количество асинхронных функций. Например, ожидание загрузки двух изображений в нашем примере можно было записать одной строкой:
let (firstImage, secondImage) = try await (firstImageTask, secondImageTask)
Пользуясь конструкцией async let
можно столкнуться с некоторыми не совсем очевидными моментами:
Async let
нельзя захватывать вescaping
замыкания. Данное ограничение ввели по причине того, что структуры, с помощью которых под капотом реализуетсяasync let
, могут храниться на стеке. В таком случае логично предположить, чтоasync let
можно использовать в неescaping
замыканиях, но на момент написания статьи это делать тоже нельзя. Это баг компилятора, когда вы читаете эту статью он возможно уже поправлен, проверить можно тут.
func asyncFunction() async { ... }
func funcWithClosure(closure: () async -> Void) { ... }
func funcWithEscapingClosure(closure: @escaping () async -> Void) { ... }
async let task = asyncFunction()
// Такой вызов будет работать в одной из следующих версий языка
funcWithClosure {
await task // compile error: Capturing 'async let' variables is not supported
}
funcWithEscapingClosure {
await task // compile error: Capturing 'async let' variables is not supported
}
В
async
функции которые вызываются с помощьюasync let
нельзя передавать переменные (любых типов).
struct Person {
var age: Int
}
func printAge(for person: Person) async {
print(person.age)
}
var person = Person(age: 23)
async let increaseAgeTask = printAge(for: person) // compile error: Reference to captured var 'person' in concurrently-executing code
При вызове async let
переменная person
не копируется, а захватывается. Это происходит из-за того, что под капотом строка с async let
преобразуется в замыкание, в котором уже вызывается async
функция. Внутрь этого замыкания нельзя захватывать переменные. Этот запрет связан уже с предотвращением состояния гонки при работе в асинхронном контексте. Подробнее поговорим об этом в следующих частях. Для устранения ошибки компиляции в примере, достаточно заменить var
на let
.
AsyncSequence. Отображение процента загрузки изображения
AsyncSequence - это протокол, с помощью которого можно обрабатывать последовательности асинхронных элементов в цикле. Как реализовать свой объект, имплементирующий AsyncSequence
, поговорим чуть позже, а сейчас давайте воспользуемся объектом, который уже подписан на этот протокол.
Воспользуемся новой функцией URLSession.bytes (доступна только с 15 iOS), которая возвращает пару значений типа (URLSession.AsyncBytes, URLResponse)
. AsyncBytes
как раз и имплементирует протокол AsyncSequence
. С помощью этого объекта можно обрабатывать данные из запроса побайтово. Давайте дополним наш контроллер лейблом для отображения процента загрузки изображения и реализуем расчет этого процента с помощью новой функции.
class ViewController: UIViewController {
// 1
private let loadedPercentLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let imageView: UIImageView = {
let view = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
func getPhotos(by albumId: Int) async throws -> [Photo] {
let url = URL(string: "https://jsonplaceholder.typicode.com/albums/\(albumId)/photos")!
let request = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(for: request)
let photos = try JSONDecoder().decode([Photo].self, from: data)
return photos
}
override func viewDidLoad() {
super.viewDidLoad()
fillView()
Task {
let photos = try await getPhotos(by: 1)
// 2
let (stream, response) = try await URLSession.shared.bytes(from: photos[0].url)
// 3
var bytes: [UInt8] = []
// 4
for try await byte in stream {
// 5
bytes.append(byte)
let currentPercent = Int(Double(bytes.count) / Double(response.expectedContentLength) * 100.0)
loadedPercentLabel.text = "\(currentPercent) %"
}
// 6
imageView.image = UIImage(data: Data(bytes))
}
}
private func fillView() {
view.backgroundColor = .black
view.addSubview(imageView)
view.addSubview(loadedPercentLabel)
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
imageView.heightAnchor.constraint(equalToConstant: 248),
imageView.widthAnchor.constraint(equalToConstant: 248),
loadedPercentLabel.bottomAnchor.constraint(equalTo: imageView.topAnchor),
loadedPercentLabel.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
])
}
}
Пробежимся по всем изменениям контроллера:
Добавили лейбл в котором будем отображать процент загрузки.
Получаем пару значений типа
(URLSession.AsyncBytes, URLResponse)
с помощью нового асинхронного методаURLSession.bytes
.Создаем переменную, в которой будем хранить все полученные байты.
В цикле обрабатываем байты из
AsyncBytes
. Байты поступают по мере загрузки, поэтому приостанавливаемся каждый раз с помощьюawait
.При получении каждого последующего байта добавляем его в массив. Затем рассчитываем процент загруженных на данный момент байт относительно ожидаемого количества (которое приходит в
URLResponse.expectedContentLength
) и обновляем текст у лейбла.Из массива байт создаем
Data
, изData
создаемUIImage
и присваиваем вUIImageView.image
.

Каждая итерация for await _ in
цикла - это потенциальная точка приостановки (suspension point). Такой цикл можно представить как просто последовательный набор await
вызовов.
Чтобы загрузка не была моментальной, мы можем добавить небольшую задержку внутри цикла:
for try await byte in stream {
try await Task.sleep(nanoseconds: 10000)
bytes.append(byte)
let currentPercent = Int(Double(bytes.count) / Double(response.expectedContentLength) * 100.0)
loadedPercentLabel.text = "\(currentPercent) %"
}
И после этого можно будет увидеть следующий результат:

AsyncStream. Перенос логики загрузки изображения
В предыдущем примере мы реализовали загрузку изображения с отображением процента прямо в контроллере, хотя ранее мы инкапсулировали логику загрузки в специальный класс ImageLoader
. Давайте перенесем в этот класс логику загрузки с прогрессом, и заодно познакомимся с сущностью AsyncStream
.
С помощью AsyncStream
мы можем легко создавать собственные асинхронные последовательности, так как он подписывается под AsyncSequence
. AsyncStream
не предполагает завершения с ошибкой. Если стрим может завершится с ошибкой, то нужно использовать альтернативу - AsyncThrowingStream
. Дополним наш класс ImageLoader
и параллельно разберемся, как он работает.
enum ImageLoaderError: Error {
case incorrectImageData
}
final class ImageLoader: Sendable {
// 1
enum LoadingState {
case loading(percent: Int)
case loaded(image: UIImage)
}
private let imageUrl: URL
init(imageUrl: URL) {
self.imageUrl = imageUrl
}
var image: UIImage {
get async throws {
let (data, _) = try await URLSession.shared.data(from: imageUrl)
guard let image = UIImage(data: data) else {
throw ImageLoaderError.incorrectImageData
}
return image
}
}
// 2
var loadingImageStream: AsyncThrowingStream<LoadingState, Error> {
// 3
AsyncThrowingStream<LoadingState, Error> { continuation in
// 4
Task { await loadImageWithProgress(progressContinuation: continuation) }
}
}
// 5
private func loadImageWithProgress(progressContinuation: AsyncThrowingStream<LoadingState, Error>.Continuation) async {
// 6
do {
let (stream, response) = try await URLSession.shared.bytes(from: imageUrl)
var bytes: [UInt8] = []
for try await byte in stream {
// 7
try await Task.sleep(nanoseconds: 10000)
bytes.append(byte)
let currentPercent = Int(Double(bytes.count) / Double(response.expectedContentLength) * 100.0)
// 8
progressContinuation.yield(.loading(percent: currentPercent))
}
guard let image = UIImage(data: Data(bytes)) else {
throw ImageLoaderError.incorrectImageData
}
// 9
progressContinuation.yield(.loaded(image: image))
progressContinuation.finish()
} catch {
// 6
progressContinuation.finish(throwing: error)
}
}
}
Создаем новый enum. C помощью него клиенты, использующие загрузку с прогрессом, будут определять текущее состояние. Всего реализуем 2 состояния.
case loading(percent: Int)
- загрузка еще в процессе, в этот кейс будем передавать текущий процент загрузки.case loaded(image: UIImage)
- загрузка завершена, в этот кейс будем передавать загруженное изображение.Добавляем новое computed property типа
AsyncThrowingStream<LoadingState, Error>
. С помощью него клиенты нашего класса смогут обрабатывать вfor try await _ in
цикле асинхронную последовательность событий с типомLoadingState
.Создаем
AsyncThrowingStream
. Для инициализации требуется передать замыкание с типом(AsyncThrowingStream<Element, Error>.Continuation) -> Void
. В continuation будем передавать наши стейты.Замыкание, которое мы передаем в инициализатор для
AsyncThrowingStream
, не асинхронное. Поэтому в нем нельзя await'ить. По этой причине заворачиваем вызов асинхронной функции вTask
.Асинхронная функция
loadImageWithProgress
включает в себя всю логику загрузки из предыдущего примера.AsyncThrowingStream
создаетcontinuation
(в который нужно передавать события или ошибку). Мы передаем этотcontinuation
в функцию. Внутри функции мы передаем в него события с помощью методаyield
.Заворачиваем всю логику из предыдущего примера в блок
do/catch
. В блокеcatch
завершаемcontinuation
с ошибкой.Задержка для наглядности загрузки.
В процессе обработки байтов из стрима от
URLSession
передаем вcontinuation
события о загрузке с текущим процентом.После завершения стрима байтов от
URLSession
передаем вcontinuation
событие об окончании загрузки с итоговым изображением. Для завершения стрима нужно дернуть методfinish
уcontinuation
. Если это не сделать, то клиенты вfor try await _ in
цикле будут бесконечно ожидать последующий событий, которых в нашем случае больше не будет.
Теперь будем использовать ImageLoader
в контроллере для загрузки изображения с процентом загрузки. Изменения коснуться только метода viewDidLoad
.
override func viewDidLoad() {
super.viewDidLoad()
fillView()
Task {
let photos = try await getPhotos(by: 1)
// 1
let loadingImageStream = ImageLoader(imageUrl: photos[0].url).loadingImageStream
// 2
for try await event in loadingImageStream {
// 3
switch event {
case let .loading(percent):
loadedPercentLabel.text = "\(percent) %"
case let .loaded(image):
imageView.image = image
}
}
}
}
Создаем
ImageLoader
и сразу запрашиваем нашAsyncThrowingStream
.В
for try await _ in
цикле обрабатываем события.В случае если событие
.loading
- отображаем новый процент. Если.loaded
- выставляем полученное изображение

Как видно из диаграммы, мы преобразуем каждый полученный байт в LoadingState
и передаем его в AsyncThrowingStream
. Этот стрим, в свою очередь, слушается (с помощью for await _ in
цикла) в контроллере, и на основе стейтов соответствующий пользовательский интерфейс.
Итоги
В этой статье мы рассмотрели несколько основных сущностей и функциональностей языка. И закрепили это все на приближенных к реальности примерах. Но это еще далеко не все, чем обзавелся Swift в версии 5.5. Для полного преисполнения асинхронностью нам еще предстоит разобраться с structurred cuncurrency и actors, которые включают внутри себя множество интересных деталей.