Combine для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое Combine и зачем он нужен?

Combine — это фреймворк Apple для реактивного программирования, представленный в 2019 году вместе с iOS 13. Он предоставляет декларативный Swift API для обработки событий с течением времени. Combine позволяет объединять операторы обработки асинхронных событий в цепочки обработки данных (pipeline).

Основная задача Combine — упростить работу с асинхронными операциями: сетевыми запросами, уведомлениями, KVO (Key-Value Observing), таймерами, делегатами и callback-функциями. Вместо разрозненных механизмов Combine предлагает единый интерфейс на основе Publisher (издатель) и Subscriber (подписчик).

Ключевые концепции:

  • Publisher — источник данных, который может отправлять значения и завершаться (успешно или с ошибкой).
  • Subscriber — получатель данных, который реагирует на события от Publisher.
  • Operator — метод, который преобразует, фильтрует или комбинирует потоки данных.
  • Subscription — связь между Publisher и Subscriber, управляющая потоком данных.

Combine тесно интегрирован с SwiftUI и Foundation, что делает его незаменимым для современных iOS-приложений.

Установка

Combine является встроенным фреймворком Apple. Для его использования не требуется устанавливать дополнительные пакеты через CocoaPods, SPM или Carthage. Он доступен начиная с iOS 13, macOS 10.15, tvOS 13 и watchOS 6.

Чтобы начать использовать Combine, достаточно импортировать его в файле Swift:

import Combine

Если вы используете Swift Package Manager для управления зависимостями, Combine уже включен в стандартную библиотеку. Для проектов, поддерживающих более старые версии ОС, можно использовать OpenCombine — open-source реализацию Combine.

Установка OpenCombine через SPM:

// В Package.swift
dependencies: [
    .package(url: "https://github.com/CombineOpenSource/Combine.git", from: "0.5.0")
]

Быстрый старт с примером кода

Рассмотрим простейший пример: создание Publisher из массива и подписка на него.

import Combine

// 1. Создаем Publisher из массива
let publisher = [1, 2, 3, 4, 5].publisher

// 2. Создаем Subscriber
let subscriber = Subscribers.Sink<Int, Never>(
    receiveCompletion: { completion in
        print("Завершено: \(completion)")
    },
    receiveValue: { value in
        print("Получено значение: \(value)")
    }
)

// 3. Подписываемся
publisher.subscribe(subscriber)

Вывод в консоль:

Получено значение: 1
Получено значение: 2
Получено значение: 3
Получено значение: 4
Получено значение: 5
Завершено: finished

Более идиоматичный способ с использованием оператора sink:

import Combine

let cancellable = [1, 2, 3, 4, 5].publisher
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })

Обратите внимание: sink возвращает AnyCancellable. Его нужно сохранять, иначе подписка будет отменена сразу после выхода из области видимости.

Основные методы/функции

1. sink(receiveCompletion:receiveValue:)

Что делает: Создает подписчика, который получает все значения и завершение от Publisher. Это самый простой способ подписаться на поток.

Параметры:

  • receiveCompletion — замыкание, вызываемое при завершении (успешном или с ошибкой). Принимает Subscribers.Completion<Failure>.
  • receiveValue — замыкание, вызываемое при получении каждого нового значения.

Пример:

let cancellable = Just("Hello, Combine!")
    .sink(receiveCompletion: { print("Готово: \($0)") },
          receiveValue: { print("Значение: \($0)") })
// Вывод: Значение: Hello, Combine!
//        Готово: finished

2. map(_:)

Что делает: Преобразует каждое значение из Publisher с помощью переданной функции. Аналог map из Swift Collections.

Параметры:

  • transform — замыкание, принимающее значение типа Output и возвращающее новое значение любого типа.

Пример:

let publisher = [1, 2, 3].publisher
let cancellable = publisher
    .map { $0 * 2 }
    .sink { print($0) }
// Вывод: 2, 4, 6

3. filter(_:)

Что делает: Пропускает только те значения, которые удовлетворяют условию в замыкании.

Параметры:

  • isIncluded — замыкание, принимающее значение и возвращающее Bool.

Пример:

let publisher = [1, 2, 3, 4, 5].publisher
let cancellable = publisher
    .filter { $0 % 2 == 0 }
    .sink { print($0) }
// Вывод: 2, 4

4. flatMap(maxPublishers:_:)

Что делает: Преобразует каждое значение в новый Publisher и "уплощает" результат в один поток. Критически важен для работы с асинхронными операциями.

Параметры:

  • maxPublishers — максимальное количество одновременно активных внутренних Publisher (по умолчанию .unlimited).
  • transform — замыкание, возвращающее новый Publisher.

Пример:

let publisher = ["Hello", "World"].publisher
let cancellable = publisher
    .flatMap { value in
        value.publisher // Каждая строка становится Publisher символов
    }
    .sink { print($0) }
// Вывод: H, e, l, l, o, W, o, r, l, d

5. merge(with:)

Что делает: Объединяет два Publisher в один, чередуя их значения в порядке поступления.

Параметры:

  • other — второй Publisher того же типа Output и Failure.

Пример:

let publisher1 = [1, 2, 3].publisher
let publisher2 = [4, 5, 6].publisher
let cancellable = publisher1
    .merge(with: publisher2)
    .sink { print($0) }
// Вывод: 1, 2, 3, 4, 5, 6 (порядок может варьироваться)

6. combineLatest(_:)

Что делает: Объединяет последние значения из двух Publisher. Каждый раз, когда любой из Publisher испускает значение, вызывается замыкание с последними значениями обоих.

Параметры:

  • other — второй Publisher.
  • transform — замыкание, принимающее два значения и возвращающее результат.

Пример:

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()
let cancellable = publisher1
    .combineLatest(publisher2) { "\($0) - \($1)" }
    .sink { print($0) }

publisher1.send(1)
publisher2.send("A")
publisher1.send(2)
// Вывод: 1 - A, 2 - A

7. zip(_:)

Что делает: Объединяет пары значений из двух Publisher по индексу. Ждет, пока оба Publisher не испустят следующее значение, и только тогда вызывает замыкание.

Параметры:

  • other — второй Publisher.
  • transform — замыкание, принимающее два значения.

Пример:

let publisher1 = [1, 2, 3].publisher
let publisher2 = ["A", "B", "C"].publisher
let cancellable = publisher1
    .zip(publisher2) { "\($0)\($1)" }
    .sink { print($0) }
// Вывод: 1A, 2B, 3C

8. debounce(for:scheduler:options:)

Что делает: Подавляет значения, которые поступают слишком часто. Пропускает значение только после того, как с момента последнего поступления прошло указанное время. Идеально для поисковых строк.

Параметры:

  • dueTime — время ожидания (например, .seconds(0.5)).
  • scheduler — планировщик, на котором выполняется задержка (обычно RunLoop.main или DispatchQueue.main).

Пример:

let subject = PassthroughSubject<String, Never>()
let cancellable = subject
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
    .sink { print("Поиск: \($0)") }

subject.send("S")
subject.send("Sw")
subject.send("Swi")
subject.send("Swif")
subject.send("Swift")
// Через 0.5 сек: Поиск: Swift

9. throttle(for:scheduler:latest:)

Что делает: Ограничивает частоту отправки значений. В отличие от debounce, throttle гарантирует, что значения будут отправляться не чаще одного раза за указанный интервал.

Параметры:

  • interval — минимальный интервал между отправками.
  • scheduler — планировщик.
  • latest — если true, отправляется последнее значение за интервал; если false — первое.

Пример:

let subject = PassthroughSubject<Int, Never>()
let cancellable = subject
    .throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true)
    .sink { print($0) }

subject.send(1)
subject.send(2)
subject.send(3)
// Через 1 сек: 3 (latest = true)

10. catch(_:)

Что делает: Обрабатывает ошибки в потоке. Если Publisher завершается с ошибкой, можно заменить его другим Publisher или значением.

Параметры:

  • handler — замыкание, принимающее ошибку и возвращающее новый Publisher.

Пример:

enum MyError: Error { case networkError }

let publisher = Fail<Int, MyError>(error: .networkError)
let cancellable = publisher
    .catch { error in
        Just(-1) // Заменяем ошибку на значение -1
    }
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) })
// Вывод: -1, finished

Реальный практический пример использования

Создадим простой менеджер для поиска пользователей через сетевой запрос с использованием Combine. Это типичная задача для iOS-приложения.

import Combine
import Foundation

// MARK: - Модель
struct User: Decodable {
    let id: Int
    let login: String
    let avatar_url: String                    

Рекомендаций