← Назад к вопросам

Что такое completion handler и как его использовать?

2.0 Middle🔥 201 комментариев
#Управление памятью#Язык Swift

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Completion Handler в iOS-разработке

Completion Handler (обработчик завершения) — это механизм в Swift/Objective-C, который позволяет передавать код (обычно в виде closure/блока) для выполнения после завершения асинхронной или ресурсоемкой операции. Это фундаментальная концепция для работы с асинхронными задачами, такими как сетевые запросы, работа с файлами, Core Data или длительные вычисления.

Основные концепции

  1. Асинхронность. Выполнение кода не блокирует основной поток (UI). Мы запускаем операцию и продолжаем работу, а когда она готова — получаем результат.
  2. Типизированный callback. Completion handler — это, по сути, функция обратного вызова, которая часто принимает параметры: результат операции (Result<T, Error>), флаг успеха (Bool) или кастомные данные.
  3. Сбегающее замыкание (@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'а на всех путях выполнения функции (включая ошибки) и аккуратную работу с потоками. Это основа отзывчивого и стабильного приложения.

Что такое completion handler и как его использовать? | PrepBro