Reactive Programming in Swift (iOS/macOS): Combine and RxSwift
Reactive programming is a paradigm based on working with asynchronous data streams and propagating changes. In the Apple ecosystem for iOS and macOS, the main tools are the native Combine framework (since iOS 13/macOS 10.15) and the third-party library RxSwift. Both allow writing declarative, concise, and easily testable code, freeing the developer from "callback hell" and complex state management.
Basic Concepts
- Publisher — a data source that can send values, completion, or an error.
- Subscriber — receives data from the Publisher and reacts to it.
- Operator — a stream transformation function (map, filter, combineLatest, debounce, etc.).
- Subject — a hybrid that can both receive and send events (PassthroughSubject, CurrentValueSubject).
- Scheduler — determines on which thread the code executes.
Combine: Native Apple Framework
Combine is built into Foundation and works with Publisher and Subscriber. Example of use for handling a text field:
import Combineimport UIKit
class ViewController: UIViewController { @IBOutlet weak var textField: UITextField! @IBOutlet weak var label: UILabel! private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() { super.viewDidLoad() // Create a Publisher from 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() // Subscribe publisher .sink { [weak self] value in self?.label.text = "You entered: \\(value)" } .store(in: &cancellables) }}In the example, debounce is used to delay the reaction to input and removeDuplicates to ignore duplicate values.
RxSwift: Powerful Alternative
RxSwift is a port of ReactiveX for Swift. It offers a wider set of operators and supports older iOS versions. The same example with RxSwift:
import RxSwiftimport 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 = "You entered: \\(text)" }) .disposed(by: disposeBag) }}Note the use of rx.text (via RxCocoa) and distinctUntilChanged() — the equivalent of removeDuplicates in Combine.
Key Operators
map/compactMap— value transformation.filter— passes only values that satisfy a condition.combineLatest— combines the latest values from multiple streams.zip— pairs elements from multiple streams.flatMap— transforms a stream into another stream (often used for network requests).catch/retry— error handling.