Очереди и фоновые задачи на Swift: как заставить приложение летать
Представьте: вы открываете приложение для заказа пиццы, нажимаете «Посмотреть меню», а экран зависает на пять секунд. Вы начинаете нервничать, пицца кажется невкусной, приложение летит в корзину. Знакомая боль? Если вы разработчик на Swift, то наверняка сталкивались с ситуацией, когда интерфейс «тормозит» из-за тяжёлых операций — загрузки картинок, парсинга JSON или сложных вычислений. Секрет плавного UX кроется в правильной работе с очередями и фоновыми задачами.
В этой статье мы разберём, как организовать многопоточность в Swift: от классического GCD и OperationQueue до современного async/await. Вы узнаете, как выгрузить тяжёлую работу из главного потока, избежать дедлоков и написать код, который будет работать быстро и предсказуемо. Поехали!
Почему очередь — это не про скуку в магазине
В Swift очередь — это не то место, где вы стоите с корзиной. Очереди (DispatchQueue) — это механизм для управления задачами. Представьте, что у вас есть несколько поваров (потоков). Один повар обслуживает зал (главный поток — UI), а остальные готовят на кухне (фоновые потоки). Если заставить официанта жарить стейк, зал встанет. То же самое с интерфейсом: все обновления UI должны происходить только на главном потоке, а всё остальное — в фоне.
В Swift есть два типа очередей:
- Serial (последовательная) — задачи выполняются одна за другой. Идеально для защиты общих ресурсов.
- Concurrent (параллельная) — задачи стартуют одновременно. Отлично для загрузки изображений или параллельных вычислений.
Система сама управляет пулом потоков, поэтому вам не нужно создавать их вручную. Просто говорите: «Сделай это в фоне, а потом верни результат на главный». И Swift делает это элегантно.
GCD (Grand Central Dispatch) — старый друг лучше новых двух
Grand Central Dispatch — это низкоуровневый API, который появился ещё в Objective-C. Он жив и здоров, и его стоит знать каждому Swift-разработчику. Основная работа происходит через DispatchQueue.
Вот самый простой пример — загрузка данных в фоне и обновление UI:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
loadImage()
}
func loadImage() {
// Уходим в фоновую очередь
DispatchQueue.global(qos: .background).async {
// Тяжёлая работа: загрузка изображения
guard let url = URL(string: "https://example.com/pizza.jpg"),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else {
return
}
// Возвращаемся на главный поток для обновления UI
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
}
Обратите внимание на qos (Quality of Service). Это приоритет задачи: .userInteractive — для анимаций, .background — для синхронизации, .utility — для индикаторов прогресса. Правильный выбор QoS помогает системе распределять ресурсы.
DispatchGroup: ждём всех, а не одного
Представьте, что вам нужно загрузить три картинки и только потом показать их. Без DispatchGroup вы бы запустили три асинхронных запроса и не знали, когда все они завершатся. Решение — группа:
func loadMultipleImages() {
let group = DispatchGroup()
var images: [UIImage] = []
let urls = ["url1", "url2", "url3"]
for urlString in urls {
group.enter() // Входим в группу
DispatchQueue.global().async {
// Загрузка изображения
if let url = URL(string: urlString),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
images.append(image)
}
group.leave() // Выходим из группы
}
}
// Уведомление о завершении всех задач
group.notify(queue: .main) {
self.imageView.image = images.first
print("Все изображения загружены!")
}
}
Используйте enter() и leave() парно — иначе группа никогда не завершится, и вы получите утечку памяти.
OperationQueue: когда нужен контроль над задачами
OperationQueue — это более высокоуровневая абстракция над GCD. Она даёт вам возможность отменять задачи, задавать зависимости и ограничивать количество параллельных операций. Если вы пишете сложный пайплайн обработки данных, OperationQueue — ваш выбор.
Пример: загружаем изображение, фильтруем его и сохраняем. Каждый шаг — отдельная операция, и они зависят друг от друга.
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2 // Не больше двух задач одновременно
let loadOperation = BlockOperation {
// Загрузка данных
Thread.sleep(forTimeInterval: 1) // Имитация задержки
print("Загрузка завершена")
}
let filterOperation = BlockOperation {
// Применение фильтра
print("Фильтрация завершена")
}
let saveOperation = BlockOperation {
// Сохранение результата
print("Сохранение завершено")
}
// Устанавливаем зависимости
filterOperation.addDependency(loadOperation)
saveOperation.addDependency(filterOperation)
queue.addOperations([loadOperation, filterOperation, saveOperation], waitUntilFinished: false)
Прелесть OperationQueue в том, что вы можете отменить все операции, если пользователь ушёл с экрана:
queue.cancelAllOperations()
Или проверить, не отменена ли операция внутри неё:
let operation = BlockOperation {
if operation.isCancelled { return }
// Тяжёлая работа
}
Современный Swift: async/await — пишем как на Python, но быстрее
Начиная с Swift 5.5, в язык пришла модель structured concurrency с ключевыми словами async и await. Если вы знакомы с Python, то почувствуете себя как дома. Код становится линейным, без вложенных замыканий, а ошибки обрабатываются через обычные do-catch.
Перепишем наш первый пример с загрузкой изображения на modern Swift:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
Task {
await loadImageAsync()
}
}
func loadImageAsync() async {
guard let url = URL(string: "https://example.com/pizza.jpg") else { return }
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
// UI обновляем на главном акторе
await MainActor.run {
self.imageView.image = image
}
}
} catch {
print("Ошибка загрузки: \(error)")
}
}
}
Обратите внимание: мы используем Task для запуска