Intro to RxSwift (Part 3)

Hi there, in this blog post we are going to talk about Operators. This is my third blog post about RxSwift(part 1, part 2) if you didn’t read them and suggest you to check them out.

Swift higher-order functions are the most similar in functionality with Operations. Swift has default higher-order functions like map, flatMap, reduce, filter, and they work on top of collections like arrays, dictionaries, sets. But remember that we working with RxSwift, and in this case, those higher-order functions work with observable sequences. The default map operator transforms an array into the new array type, whereas RxSwift map operator transforms the observable sequence into new observable sequence type. So you can see they are similar.

There are 3 main types of operators: Filtering, Transforming,  Combining. Filtering operators allow you ignore or skip some elements from an observable sequence. Transforming operators help you to change types of observable sequence, as an example, you can have an observable sequence of button taps which you can transform to button tags. So, when the button gets tap your observable will return the buttons tag. And Combining operators allows you to merge multiple sequences into one sequence or help you compose sequences in a certain way, for example, you have 2 observable sequences and you want to get values of the sequence which emits some value first and don’t listen to other anymore.

In this section RxMarbels will help you a lot, to get the visual understanding. So, go to rxmarbles.com and look at the left side, there you can see the section with the list of operators divided into sections. And in the middle of the screen, some observable sequences illustrated by right arrows with numbered circles on them. Numbered circles illustrate emitting values. Top arrows are input observables and resulting observable at the bottom.

Filtering Operators

Let’s start with simple filter operator. It’s very similar to high-order filter function, only it works on observable sequences. Here is the quick example:

let bag = DisposeBag()
    
Observable.of(1,9,2,8,3,7,4,6,5)
    .filter{ $0 > 5 }
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

That is an observable sequence of type integer, followed by filter operator which filters emitted values which are more than 5. Easy. And the output:

9
8
7
6

The next operator is ignoreElements. This operator ignores all onNext events, catches only onError or onCompleted events. And if you look at the return type of ignoreElement operator you will see that it isn’t an observable. And the type it returns doesn’t have the onNext parameter in subscribe method. Example:

Observable.of(1,9,2,8,3,7,4,6,5)
    .ignoreElements()
    .subscribe(onCompleted: {
        print("Completed")
    })

And the output:

Completed

The next operator you often will be using is the distinctUntilChanged operator. It checks if the previously emitted value of an observable is equal to the new value, then it filters it.

let bag = DisposeBag()
    
Observable.of("Hi", ",", "Ther", "There", "!", "!", "?")
    .distinctUntilChanged()
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

In this example, the observable emits 2 “!” symbols. But distinctUntilChanged operator filters second exclamation point.

Hi
,
Ther
There
!
?

So as you can see, it skipped second “!”, and printed the next “?” symbol.

Now let’s move on to more advanced filter operators. Imagine you need to take only first 3 emitted values from observable, and you don’t care about the rest. In this case, take operator will come to the rescue. Take takes the number of elements your subscription should care about. Let’s look at the example:

let bag = DisposeBag()
    
Observable.of("Hello", "World", "!", "I", "will", "be", "skipped")
    .take(3)
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

The observable sequence of type String, followed by filtering operator take with input value 3. This means that subscribers will get only first 3 emitted elements, no more. And the output:

Hello
World
!

But what if you want to take elements while sticking to some condition, and if the condition got violated your subscription won’t get any further events?  The operator you need is the takeWhile operator, it takes some function that returns bool as an input value. And as you probably guessed it works similarly to take operator subscriptions get events while sticking to some condition, and if the condition gets violated, subscription won’t get any upcoming events.

let bag = DisposeBag()
    
Observable.of(2, 1, 3, 4, 2, 0, 7)
    .takeWhile { $0 < 4 }
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

In this example, the condition is to take values until their value is more than 4. Don’t mistake this with filter operator.

2
1
3

So once the emitted value more than 4, the subscription does not get any values from an observable, even if the next value stick with the condition. That’s the main difference between filter and takeWhile operators.

The next filtering operator is opposite to take operator. It’s called skip operator. If take takes events until condition violated, skip skips events while sticking to some condition. Let’s take first take example, but with the skip operator:

let bag = DisposeBag()
    
Observable.of("Hello", "World", "!", "I", "will", "be", "skipped")
    .skip(3)
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

Almost the same code, with an exception .skip(3) method. If .take(3) outputs “Hello world!”, the .skip(3) outputs an opposite result:

I
will
be
skipped

And almost the same with skipWhile operator. There are more take and skip operators you can check out, so I advise you to go to RxSwift GitHub and download the playground it’s incredibly helpful.

Transforming operators

There are two most commonly used operators: map and flatMap. They work similarly to Swift high-order functions. And if you know how to use them with arrays, it won’t be a problem for you to understand how they operate. Let’s start with simple map operator’s example:

let bag = DisposeBag()
Observable.of(1, 2, 3)
    .map { "The value transformed to: \($0 + $0)" }
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

There is a simple observable sequence of type integer with next events: 1, 2, 3. And the map operator transforms those events in following way:

  1. It changes the type of observable from Integer to String
  2. It changes the value of events to “The value transformed to: (value of event added with itself)”

And the output we get:

The value transformed to: 2
The value transformed to: 4
The value transformed to: 6

The flatMap operator needs some time to grasp, it’s hard to get it at first. The first thing to remember flatMap has to return an observable sequence, and the most common example of using flatMap is taking out an observable sequence out of observable sequence 🤔.  So look at this example:

let bag = DisposeBag()
    
let observable1: Observable<Int> = Observable.of(1,2,3)
let observable2: Observable<Int> = Observable.of(4,5,6)
let observable3: Observable<Int> = Observable.of(7,8,9)
    
let observableOfObservables: Observable<Observable<Int>> = Observable
    .of(observable1,
        observable2,
        observable3)
    
let flattedObservable: Observable<Int> = observableOfObservables.asObservable().flatMap { $0 }
    
flattedObservable
    .subscribe(onNext: { (value) in
        print(value)
    })
    .disposed(by: bag)

There are three simple integer observable sequences, and one super observable sequence holding those 3. Now look at the type of observableOfObservables it is Observable<Observable<Int>>. If you know how flatMap operates in swift, you should get a clue. And if you don’t the hint in word flat. It converts the embedded sequences into one flat sequence. Now, look at the type of flattedObservable it is Observable<Int>. So it took the type that flatMap operator returned, in this case result of flatMap is  Observable<Int>. And the output is:

1
2
4
3
5
7
6
8
9

Those 3 embedded observables converted to one flat observable stream.

Combining operators

Combining operators implies merging multiple observable sequences into a single sequence. And the simplest one is the startWith operator. This operator puts some events before the sequence starts to emit events. Look at the example:

let bag = DisposeBag()

Observable.of("1", "2", "3", "3")
    .startWith("these words")
    .startWith("It", "started", "with")
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

As you can see we can chain the operators one after another, and the startWith operator can take multiple elements as an input. The observable itself has next “1”,”2″,”3″,”3″ events, followed by startWith operator meaning that the value inside the operator will emit before the observable sequence. The result:

It
started
with
these words
1
2
3
3

If you look carefully at the result, you should notice that “these words” string appeared after “it”,”started”,”with” events, whereas in the statement we put it before them. So you need to keep in mind that startWith can be chained on a last-in-first-out basis.

The next operator we are going to take a look is the merge operator. It merges multiple source observable sequences into one observable sequence. Imagine you have multiple observable sequences of the same type and you want to observe those sequences as one sequence. This is where the merge operator will be handy. Check out this example:

let bag = DisposeBag()

let subject1 = PublishSubject<String>()
let subject2 = PublishSubject<String>()

Observable.of(subject1, subject2)
    .merge()
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

subject1.onNext("A")

subject1.onNext("B")

subject2.onNext("1")

subject2.onNext("2")

subject1.onNext("AB")

subject2.onNext("12")

We have 2 subjects of the same type, and we want to observe both of them as one. And guess what is the result:

A
B
1
2
AB
12

As you can see it printed all emitted events of both observables.

The next very useful operator is zip. It takes two observable sequences(can be different types), merges them into one observable sequence with one interesting feature: The resulting observable sequence won’t emit any event until it can make a pair of two events from both sequences. Look at the example:

let bag = DisposeBag()

let stringSubject = PublishSubject<String>()
let intSubject = PublishSubject<Int>()

Observable.zip(stringSubject, intSubject) { stringElement, intElement in
    "\(stringElement) \(intElement)"
    }
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

stringSubject.onNext("A")
stringSubject.onNext("B")

intSubject.onNext(1)
intSubject.onNext(2)

stringSubject.onNext("C")
intSubject.onNext(3)

stringSubject.onNext("Won't show up")

We have 2 observable sequences of different type merged with zip operator, and printing the result of that merge. And some events pushed to those 2 observable sequences separately. So, look at the output:

A 1
B 2
C 3

In the example, stringSubject emitted 2 events before intSubject emitted any. But in the output, we can see that zipped observable sequence didn’t print any events until both observable sequences emitted events. And also it following the order events were emitted. And the last string “Won’t show up” event, did not print out, because there wasn’t any pair for that event at the time.

And the last combining operator I want to tell you about is combineLatest operator. This operator takes two observable sequences(can be different types too), waits until both of them emit events, and merges them into one observable sequence. It similar and at the same time very different from zip operator. If the zip pairs events in followed order, and won’t emit any event if there isn’t a pair of events from both observable sequence, combineLatest can pair events without waiting for new events and pairs new event from the first/second sequence with the old second/first sequence event. Let’s take the same example we used with zip operator:

let bag = DisposeBag()

let stringSubject = PublishSubject<String>()
let intSubject = PublishSubject<Int>()

Observable.combineLatest(stringSubject, intSubject) { stringElement, intElement in
    "\(stringElement) \(intElement)"
    }
    .subscribe(onNext: { print($0) })
    .disposed(by: bag)

stringSubject.onNext("A")
stringSubject.onNext("B")

intSubject.onNext(1)
intSubject.onNext(2)

stringSubject.onNext("C")
intSubject.onNext(3)

stringSubject.onNext("Won't show up")

The same example with only one exception, combineLatest instead of zip operator. Now let’s look at the output:

B 1
B 2
C 2
C 3
Won't show up 3

It didn’t print out “A”, but it did print out the “Won’t show up” message. When we pushed “A” event, it didn’t have the pairing event for it, so it didn’t log it. Next “B” event gets pushed, but still no pair for that. And next second sequence pushes “1” event, and this time there is a pair for it, it is current event “B” of the first sequence. The second sequence pushed again and this time event “2”, it pairs with that same “B” event. And when the first sequence pushes “C” event, it gets paired with second sequence’s event “2”. And so on, you see the logic.

Conclusion

Those examples demonstrated just as small part of the operators. So there more of them, and you need to learn them if you want to use RxSwift at its full power. But, as you can see the names of those operators are pretty self-explanatory.

And if you want to learn more go to RxSwift GitHub repository. To get the visual understanding of operators check out RxMarbles.com.

Leave a comment if you have any questions, or just want to say “Hi”, I will be glad to talk.

Subscribe to get notified of new posts.

Thank you

 
Loading
Please follow and like us:

1 comments on “Intro to RxSwift (Part 3)”

    • Benny
    • 06.01.2019
    Reply

    Thanks a lot for this article. 🙏
    It was very helpful! Very good explanation.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.