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.