Что такое completion handler и как его использовать?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Completion Handler в iOS-разработке
Completion Handler (обработчик завершения) — это механизм в Swift/Objective-C, который позволяет передавать код (обычно в виде closure/блока) для выполнения после завершения асинхронной или ресурсоемкой операции. Это фундаментальная концепция для работы с асинхронными задачами, такими как сетевые запросы, работа с файлами, Core Data или длительные вычисления.
Основные концепции
- Асинхронность. Выполнение кода не блокирует основной поток (UI). Мы запускаем операцию и продолжаем работу, а когда она готова — получаем результат.
- Типизированный callback. Completion handler — это, по сути, функция обратного вызова, которая часто принимает параметры: результат операции (
Result<T, Error>), флаг успеха (Bool) или кастомные данные. - Сбегающее замыкание (
@escaping). Поскольку операция асинхронная, closure должен «сбежать» из scope функции, которая его принимает, и быть сохранен для вызова позже. В Swift это маркируется атрибутом@escaping.
Зачем он нужен?
Без completion handler'а при сетевом запросе интерфейс «зависнет» на время ожидания ответа сервера. С ним — мы инициируем запрос, UI остается отзывчивым, а когда данные приходят, мы просто обновляем экран в переданном closure.
Примеры использования в Swift
1. Базовый пример с кастомной функцией
func fetchUserProfile(userId: String, completion: @escaping (Result<UserProfile, Error>) -> Void) {
// 1. Имитируем асинхронный сетевой запрос
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
let isSuccess = Bool.random()
// 2. По завершении "запроса" вызываем completion
if isSuccess {
let profile = UserProfile(name: "Иван Иванов", email: "ivan@test.ru")
completion(.success(profile))
} else {
let error = NSError(domain: "FetchError", code: 404, userInfo: nil)
completion(.failure(error))
}
}
// 3. Функция завершается сразу, не дожидаясь конца async-блока
}
// 4. Где-то во ViewModel или ViewController вызываем:
fetchUserProfile(userId: "123") { result in
// 5. Этот код выполнится ТОЛЬКО когда запрос завершится (~через 1 сек.)
switch result {
case .success(let profile):
DispatchQueue.main.async {
// Обновляем UI на главном потоке
self.nameLabel.text = profile.name
}
case .failure(let error):
print("Ошибка загрузки: \(error.localizedDescription)")
}
}
2. Работа с системными API (URLSession)
func loadData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// Этот closure — и есть completion handler от URLSession
completion(data, response, error)
}
task.resume()
}
// Вызов с современным Result-типом
func loadDataModern(from url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(MyError.noData))
return
}
completion(.success(data))
}.resume()
}
3. Кастомный handler с несколькими параметрами
typealias FetchCompletion = (_ items: [String], _ nextPageToken: String?, _ error: Error?) -> Void
func fetchPaginatedData(pageToken: String?, completion: @escaping FetchCompletion) {
// Асинхронная логика...
completion(["Item1", "Item2"], "next_page_123", nil)
}
Ключевые практики и подводные камни
1. Всегда возвращайтесь на главный поток для UI
completion { result in
DispatchQueue.main.async {
// Работа с UI-элементами ТОЛЬКО здесь
self.tableView.reloadData()
}
}
2. Избегайте цикла сильных ссылок (retain cycle)
Замыкания (closure) захватывают (capture) ссылки на объекты. Если ViewController хранит ссылку на операцию, а операция в своем completion handler ссылается на self (ViewController) — возникает retain cycle, и память не освобождается.
Решение: использовать [weak self] или [unowned self].
fetchUserProfile(userId: "123") { [weak self] result in
// self стал опциональным
guard let self = self else { return } // Проверяем, "жив" ли еще self
DispatchQueue.main.async {
self.updateUI(with: result) // Безопасное обращение
}
}
3. Опциональность completion handler'а
Иногда нужно сделать вызов completion опциональным:
func doWork(completion: (() -> Void)? = nil) {
// Логика...
completion?() // Вызов только если передан
}
4. Переход от Completion Handlers к async/await (Swift 5.5+)
В современных версиях Swift те же задачи решаются элегантнее с помощью async/await:
// Старая функция с completion
func fetchOld(completion: @escaping (Result<Data, Error>) -> Void)
// Новая функция с async
func fetchNew() async throws -> Data
// Использование
Task {
do {
let data = try await fetchNew() // Прямолинейно, нет closure
await MainActor.run { updateUI(with: data) }
} catch {
print(error)
}
}
Completion handler остается краеугольным камнем асинхронного программирования в iOS, даже с приходом async/await. Понимание его работы необходимо для:
- Легаси-кода (подавляющее большинство проектов)
- Создания оберток над старыми API
- Глубокого понимания потока выполнения в приложении
Правильное использование включает: выбор слабых ссылок, обязательный вызов handler'а на всех путях выполнения функции (включая ошибки) и аккуратную работу с потоками. Это основа отзывчивого и стабильного приложения.