Реактивное программирование в 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]&