Реактивное программирование в Swift: Combine и RxSwift для iOS/macOS

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

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

Начать курс

Реактивное программирование в Swift (iOS/macOS): Combine и RxSwift

Реактивное программирование — это парадигма, в основе которой лежит работа с асинхронными потоками данных и распространение изменений. В экосистеме Apple для iOS и macOS основными инструментами являются нативный фреймворк Combine (с iOS 13/macOS 10.15) и сторонняя библиотека RxSwift. Оба позволяют писать декларативный, лаконичный и легко тестируемый код, избавляя разработчика от «callback hell» и сложного управления состоянием.

Основные понятия

  • Publisher (Издатель) — источник данных, который может отправлять значения, завершение или ошибку.
  • Subscriber (Подписчик) — получает данные от Publisher и реагирует на них.
  • Operator (Оператор) — функция преобразования потока (map, filter, combineLatest, debounce и др.).
  • Subject — гибрид, который может как получать, так и отправлять события (PassthroughSubject, CurrentValueSubject).
  • Scheduler (Планировщик) — определяет, на каком потоке выполняется код.

Combine: нативный фреймворк Apple

Combine встроен в Foundation и работает с Publisher и Subscriber. Пример использования для обработки текстового поля:

import Combine
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var label: UILabel!
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Создаем Publisher из NotificationCenter
        let publisher = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: textField)
            .compactMap { ($0.object as? UITextField)?.text }
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .removeDuplicates()
        
        // Подписываемся
        publisher
            .sink { [weak self] value in
                self?.label.text = "Вы ввели: \(value)"
            }
            .store(in: &cancellables)
    }
}

В примере используется debounce для задержки реакции на ввод и removeDuplicates для игнорирования повторяющихся значений.

RxSwift: мощная альтернатива

RxSwift — это порт ReactiveX для Swift. Он предлагает более широкий набор операторов и поддерживает более старые версии iOS. Тот же пример с RxSwift:

import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var label: UILabel!
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        textField.rx.text
            .orEmpty
            .debounce(.milliseconds(500), scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .subscribe(onNext: { [weak self] text in
                self?.label.text = "Вы ввели: \(text)"
            })
            .disposed(by: disposeBag)
    }
}

Обратите внимание на использование rx.text (через RxCocoa) и distinctUntilChanged() — аналог removeDuplicates в Combine.

Ключевые операторы

  • map / compactMap — преобразование значений.
  • filter — пропускает только значения, удовлетворяющие условию.
  • combineLatest — объединяет последние значения из нескольких потоков.
  • zip — попарно объединяет элементы из нескольких потоков.
  • flatMap — преобразует поток в другой поток (часто используется для сетевых запросов).
  • catch / retry — обработка ошибок.

Управление памятью и отписка

В Combine для хранения подписок используется Set<AnyCancellable>, в RxSwift — DisposeBag. При уничтожении контейнера все подписки автоматически отменяются, предотвращая утечки памяти.

Реактивный UI и сетевые запросы

Реактивное программирование идеально подходит для связывания UI с данными. Пример с URLSession в Combine:

import Combine

struct Post: Codable {
    let id: Int
    let title: String
}

func fetchPosts() -> AnyPublisher<[Post], Error> {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
    return URLSession.shared.dataTaskPublisher(for: url)
        .map(\.data)
        .decode(type: [Post].self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

То же самое с RxSwift:

import RxSwift

func fetchPosts() -> Observable<[Post]&                    

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