Signal and Relay in RxCocoa 4

MercariEng
Making Mercari
Published in
4 min readDec 27, 2017

Hi, this is Minh Vu Nhat and I’m an iOS Engineer on the Souzoh team.

Souzoh develops a number of new products for the Mercari group, but almost all of their iOS apps use the RxSwift reactive library. A few new classes were implemented in RxSwift 4/RxCocoa 4, so I’d like to talk about them and their implementations here.

Today, I’ll be introducing the Signal, PublishRelay, and BehaviorRelay classes.

Signal

Those of you who frequently use RxCocoa already know this, but there’s a trait called Driver which is provided for reactive programming in the UI layer.

Signal is similar to Driver, but SharingStrategy is different. Driver replays once when subscribed to, but Signal does not.

Both Driver and Signal have streams that:

  • never return an error
  • guarantee they’ll be on the main thread

As such, they are recommended for ViewController UI binding.

The replays for Driver and Signal at the time of subscription look like this:

It’s said that Signal is convenient for streams where being replayed is a problem. However, in my experience, I’ve had more cases where not being replayed is a problem. Therefore, I’ve always used Driver up until now.

For example, in the event that the timing of the ViewModel/ViewController binding and the first trigger are different, a bug often occurs where the first event doesn’t get processed if the trigger happens before the binding. But if you use Driver, you don’t have to worry about whether the trigger or binding comes first.

Relay Classes

Relay classes were first implemented in RxCocoa 4. They guarantee the following properties:

  • never produces an error
  • never completes

In the newest version, the PublishRelay and BehaviorRelay classes have been implemented. PublishRelay is a wrapper for PublishSubject, and BehaviorRelay is a wrapper for BehaviorSubject.

Relay classes are based on the ObservableType protocol, but an important difference is, unlike Subject classes, they aren’t based on ObserverType. We don’t want to receive complete/error from an Observable, so it’s recommended to not use bind(to:) methods.

PublishRelay

PublishRelay is a wrapper for PublishSubject.

public final class PublishRelay<Element>: ObservableType {
private let _subject: PublishSubject<Element>
public init() {
_subject = PublishSubject()
}
}

BehaviorRelay

BehaviorRelay is a wrapper for BehaviorSubject.

public final class BehaviorRelay<Element>: ObservableType {
private let _subject: BehaviorSubject<Element>

public var value: Element {
return try! _subject.value()
}

public init(value: Element) {
_subject = BehaviorSubject(value: value)
}
}

Deprecating Variable

BehaviorRelay has a value just like a Variable, but BehaviorRelay’s value is read-only. You cannot assign .value = like you can with Variable.

Assigning the value of a Variable is an imperative programming-style command, so I don’t think it belongs in Reactive’s declarative programming environment in the first place. In the future, Variable will be deprecated and become an alias for BehaviorRelay, so I think it’s better to start dealing with that now.

When you want to send an event to a Relay class

There are two ways to send an event to a Relay.

The first is to use the accept method to have the Relay receive the event directly.

someRelay.accept(someEvent)

The other is either binding to PublishRelay from Signal, or binding to BehaviorRelay from Driver.

someSignal
.emit(to: somePublishRelay)
.disposed(by: disposeBag)

someDriver
.drive(someBehaviorRelay)
.disposed(by: disposeBag)

PublishRelay is a wrapper for PublishSubject so it doesn’t replay, and BehaviorRelay is a wrapper for BehaviorSubject so it only replays the last event.

// Signal+Subscription.swift
public func emit(to relay: PublishRelay<E>) -> Disposable {
return emit(onNext: { e in
relay.accept(e)
})
}
// Driver+Subscription
public func drive(_ relay: BehaviorRelay<E>) -> Disposable {
MainScheduler.ensureExecutingOnScheduler(errorMessage: errorMessage)
return drive(onNext: { e in
relay.accept(e)
})
}

What’s so good about not returning complete?

It’s probably obvious, but it comes in handy in those situations where returning complete causes problems.

When binding from multiple streams to PublishSubject (which is shared across apps), if any one of those streams sends a complete, it’s possible that PublishSubject may terminate.

In order to prevent .complete coming from the original streams, the below extension is commonly used:

extension SharedSequence {
public func neverComplete() -> Driver<E> {
return asObservable()
.concat(Observable.never())
.asDriverIgnoringError()
}
}

postItemButton.rx.tap.asDriver()
.map { InterceptedEvent.postOffer(handler: { /*...*/ }) }
.neverComplete()
.drive(ProfileManager.triggerEvent)
.disposed(by: disposeBag)

However, it is not guaranteed that neverComplete() will be called, so we cannot check for bugs at the time of compilation. If we replace the shared PublishSubject with PublishRelay, we can guarantee that we will never get a .complete.

postItemButton.rx.tap.asDriver()
.map { InterceptedEvent.postOffer(handler: { /*...*/ }) }
.drive(ProfileManager.triggerRelay)
.disposed(by: disposeBag)

Conclusion

With the addition of Signal, we have many more reactive expressions in the UI layer, and we can now consider whether or not we’ll get Replay events during the development process and choose classes accordingly. In addition, by using PublishRelay and BehaviorRelay for bindings, we can guarantee that we will not get errors or completion events at the time of compilation. This makes it easier to detect not only crashes while running, but also any bugs caused by unexpected behavior.

For the domain layer and network layer, I think it’s totally fine to use Raw Observables, and it might actually be better to have explicit errors and completion events. However, for the UI layer, I believe we should use RxCocoa’s Traits: Driver, Signal, and Relay.

--

--