Введение в RxSwift. Часть 3. Subjects


Логотип проекта ReactiveX

Subjects (субъекты) расширяют поведение обозреваемых последовательностей, наделяя их свойствами обозревателя. Так как это обозреватель, то он может подписываться на обозреваемые последовательности, а благодаря, тому что он сам является таковым, он может ретранслировать полученные значения, а также транслировать новые. Так справедливо будет сказать, что субъекты выступают своеобразным прокси.

Существует 4 типа субъектов:

  • PublishSubject - создается пустым и транслирует подписчикам только новые значения;
  • BehaviorSubject - создается с некоторым изначальным значением и ретранслирует (а далее последнее полученное значение) подписчикам;
  • ReplaySubject - инициализируется с некоторым размером буфера, все элементы, входящие в этот буфер, будут ретранслированы подписчикам;
  • AsyncSubject - хранит текущее значение и ретранслирует его подписчикам.

Рассмотрим каждый вид подробнее. Чтобы нам было удобнее экспериментировать, напишем вспомогательную функцию test, которая поможет нам проиллюстрировать поведение каждого вида субъектов.

func test<T>(_ subject: T, count: Int) where T: SubjectType, T.SubjectObserverType.E == Int {
    var subscriptionResults = [String]()
    
    for i in (0...count) {
        subscriptionResults.append("--")
        
        subject
            .subscribe(
                onNext: { value in
                    subscriptionResults[i] += " \(value) --"
                },
                onCompleted: {
                    print("\(subject.self)Subscription\(i): \(subscriptionResults[i])>")
                })
            .disposed(by: disposeBag)
        
        subject.asObserver().onNext(i)
    }
    
    subject.asObserver().onCompleted()
}

Принцип работы очень простой: для переданного субъекта, мы создаем подписку (как для обозреваемой последовательности) и передаём ему значения (как для обозревателя), делаем это заданное количество раз, записывая результат в строку. В результате получим диаграмму значений для каждой подписки. Отличия в этих диаграммах проиллюстрируют нам отличия в поведении различных субъектов.

PublishSubject

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

Объявим его:

let publishSubject = PublishSubject<Int>()

Результат выполнения метода test(publishSubject, count: 2) (вывод отформатирован для наглядности):

PublishSubjectSubscription0: -- 0 -- 1 -- 2 -->
PublishSubjectSubscription1:      -- 1 -- 2 -->
PublishSubjectSubscription2:           -- 2 -->

Мы видим, что для Subscription1 напечатались все 3 переданные значения, тогда как для Subscription3 только последнее, что иллюстрирует описанное поведение.

BehaviorSubject

BehaviorSubject ведёт себя аналогично PublishSubject, с той лишь разницей, что он ретранслирует последнее полученное значение новым подписчикам.

Объявим его:

let behaviorSubject = BehaviorSubject<Int>(value: 0)

Можно заметить, что здесь нам потребовался дополнительный параметр value - некое начальное значение, которое будет первым отдано подписчику, пока не придут другие значения.

Результат выполнения метода test(behaviorSubject, count: 5):

BehaviorSubjectSubscription0: -- 0 -- 0 -- 1 -- 2 -- 3 -- 4 -- 5 -->
BehaviorSubjectSubscription1:      -- 0 -- 1 -- 2 -- 3 -- 4 -- 5 -->
BehaviorSubjectSubscription2:           -- 1 -- 2 -- 3 -- 4 -- 5 -->
BehaviorSubjectSubscription3:                -- 2 -- 3 -- 4 -- 5 -->
BehaviorSubjectSubscription4:                     -- 3 -- 4 -- 5 -->
BehaviorSubjectSubscription5:                          -- 4 -- 5 -->

Здесь видим, что первый подписчик получил начальное значение, которое мы задали при инициализации, а каждый последующий подписчик помимо новых значений, получает ещё одно, предыдущее.

ReplaySubject

Этот тип можно использовать в ситуациях, когда нужно закешировать некоторый буфер значений, а затем ретранслировать его новым подписчикам.

Объявим его:

let replaySubject = ReplaySubject<Int>.create(bufferSize: 2)

Результат выполнения метода test(replaySubject, count: 7):

ReplaySubscription0: -- 0 -- 1 -- 2 -- 3 -- 4 -- 5 -- 6 -- 7 -->
ReplaySubscription1: -- 0 -- 1 -- 2 -- 3 -- 4 -- 5 -- 6 -- 7 -->
ReplaySubscription2: -- 0 -- 1 -- 2 -- 3 -- 4 -- 5 -- 6 -- 7 -->
ReplaySubscription3:      -- 1 -- 2 -- 3 -- 4 -- 5 -- 6 -- 7 -->
ReplaySubscription4:           -- 2 -- 3 -- 4 -- 5 -- 6 -- 7 -->
ReplaySubscription5:                -- 3 -- 4 -- 5 -- 6 -- 7 -->
ReplaySubscription6:                     -- 4 -- 5 -- 6 -- 7 -->
ReplaySubscription7:                          -- 5 -- 6 -- 7 -->

Наблюдается поведение, аналогичное BehaviorSubject, с той лишь разницей, что каждый новый подписчик получает не одно предыдущее значение, а их количество, указанное при создании (bufferSize).

AsyncSubject

Этот тип удобно использовать, когда нужно получить только последнее значение, по завершению работы Observable.

Объявим его:

let asyncSubject = AsyncSubject<Int>()

Результат выполнения метода test(asyncSubject, count: 3):

AsyncSubjectSubscription0: -- 3 -->
AsyncSubjectSubscription1: -- 3 -->
AsyncSubjectSubscription2: -- 3 -->
AsyncSubjectSubscription3: -- 3 -->

Куда исчез Variable?

Вдумчивый читатель мог отметить, что я ничего не рассказал о типе Variable, который можно встретить в аналогичных статьях других авторов. В настоящее время, этот тип объявлен устаревшим, и авторы RxSwift рекомендуют использовать BehaviorRelay вместо него. Поэтому для полноты картины коротко рассмотрим Relay-классы.

Классы Relay

Классы Relay были впервые реализованы в RxCocoa, но в новых версиях RxSwift их перенесут в отдельный фреймворк RxRelay. Главное их отличие: они никогда не транслируют события ошибок и завершения. Они удовлетворяют протоколу ObservableType, но в отличие от Subjects, не удовлетворяют ObserverType.

PublishRelay

Является оберткой над PublishSubject c тем же принципом работы.

Пример использования:

let publishRelay = PublishRelay<Int>()

publishRelay.subscribe(onNext: { value in
    print(value)
})

publishRelay.accept(1)
publishRelay.accept(2)

BehaviorRelay

Является оберткой над BehaviorSubject с тем же принципом работы.

Пример использования:

let behaviorRelay = BehaviorRelay<Int>(value: 0)

behaviorRelay.subscribe(onNext: { value in
    print(value)
})

behaviorRelay.accept(1)
behaviorRelay.accept(2)

Выбор того или иного типа диктуется потребностями архитектуры приложения и тут сложно дать конкретные рекомендации. Если говорить о доменных уровнях приложения, то на сервисном уровне лучше использовать Observables и Subjects, где информация об ошибках и завершении последовательности может быть полезна. На уровне пользовательского интерфейса лучше использовать Relay-классы (к примеру, нажатия на кнопку не будут производить ошибки и у них нет момента завершения) и мы ещё к ним вернёмся, когда будем подробнее рассматривать RxCocoa.

Исходный код примеров