RxSwift: Better Error Handling With CompactMap

} }}Here we replace our catchErrorJustReturn() function with a materialize() function inside of our flatMapLatest().

We then assign the end result of that sequence to a temporary variable named loading.

If the load operation succeeds, then the result is of type Event.

next<[String]>.

If it’s an error, then it’s Event.

error(Swift.

Error).

Next, we branch our temporary observable stream into two separate observable streams: one that will contain our data, and another that contains our error, if any.

Our Data StreamWe access the data hidden inside the event using .

element, which returns an optional value of type Element, which is the original type of our data value before we materialized it.

In this example, that converts the data side of the stream to an optional array of strings.

We then filter our optional value on nil, with the end result being that we only let actual data values pass down the stream.

If the event doesn’t contain data, it’s blocked and any subscribers to our observable see nothing.

Finally, we explicitly unwrap the value that we know exists, giving us the data we had in the first example, an Observable<[String]>.

Our Error StreamWe do the same for the error branch, using $0.

error?.

localizedDescription to see if our materialized stream contains an error event and in the proces extracting the localized error returned by the API.

We then do the same filter and explicit unwrap sequence as we did for our data.

The result?.An Observable<String> that contains our error message and fires whenever an error occurs.

So.

One trigger, a data observable, an error observable, and we didn’t let an error escape the flatMap and ruin our observable chain.

Life is good.

Except…I don’t know about you, but to me the above code seems a bit redundant in the sequence of operations we have to perform on both the data side and the error side of our observable chain.

So how to fix it?Well, this is where RxSwift 5 comes to the rescue.

RxSwift 5 and CompactMapRxSwift 5 added a new feature to observable streams that mirrors the addition of a feature added to Swift Sequences: compactMap.

In Swift, using compactMap() on an array of optional values returns a new array of values with all of the optional values filtered out.

To put it another way, it can convert a type of Array<[String?]> to an Array<[String]>.

In RxSwift, compactMap() performs a similar function, letting us map a stream’s elements to optional values and then filtering out any resulting optional (nil) values in the process.

The RxSwift 5 CompactMap ExampleApply compactMap() to our last example and we get the following…class CompactMapErrorViewModel { var data: Observable<[String]>!.var error: Observable<String>!.var dataService = DataService() init(load: Observable<Void>) { let loading = load .

flatMapLatest { [unowned self] _ in self.

dataService.

load().

materialize() } .

observeOn(MainScheduler.

instance) .

share() data = loading .

compactMap { $0.

element } error = loading .

compactMap { $0.

error?.

localizedDescription } }}In each case, compactMap() replaces our map, filter, map sequence on data and error with a single operator.

Much cleaner.

Regarding observeOn and PerformanceOne might ask about the placement of .

observeOn(MainScheduler.

instance) in our shared loading sequence.

Wouldn’t we be better off moving our observeOn() function and adding one beneath each compactMap() operation?.That would appear to keep more of our processing code in the background, before we switch our final result back to the main processing thread… and you’re correct, it would.

It would also add not one, but two fairly heavy background to foreground thread context switches to our code.

As each compactMap() function is pretty lightweight, in this particular instance I deemed it better to perform my context switch only once.

If, on the other hand, I needed to perform some operation on each and every element of my loaded array, I probably would have went the other way and moved observeOn() to each of my branches.

class CompactMapOperationViewModel { var data: Observable<[String]>!.var error: Observable<String?>!.var dataService = DataService() init(load: Observable<Void>) { let loading = load .

flatMapLatest { [unowned self] _ in self.

dataService.

load().

materialize() } .

share() data = loading .

compactMap { $0.

element?.

map { "Hello, ($0)" } .

observeOn(MainScheduler.

instance) error = loading .

map { $0.

error?.

localizedDescription } .

observeOn(MainScheduler.

instance) }If you have sharp eyes, you might also note that our last compactMap() on our now optional error observable was changed to a map() instead.

In this revised case we’re letting nil error string events pass, which in turn lets us clear out any previous error message in our view controller’s error label when data is successfully loaded.

Both cases should make it clear that you need to think about your code and UI requirements and the operations that you’re performing, and adjust your coding patterns accordingly.

CompactMap / Unwrap Bonus RoundYou may have come across a library of RxSwift community extensions called RxSwiftExt.

If so, you might have seen or even used an RxSwiftExt operator named unwrap().

Unwrap, to quote the code: “Takes a sequence of optional elements and returns a sequence of non-optional elements, filtering out any nil values.

”Unwrap is a handy little function in RxSwift, given just how prevalent optionals are used in the Swift language.

In fact, it’s so handy that it’s often been proposed that it be added to the core RxSwift language itself.

Look inside the implementation, however, and you’ll see something a bit familiar…public func unwrap<T>() -> Observable<T> where Element == T?.{ return self.

filter { $0 != nil }.

map { $0!.}}Yep.

It’s the same filter, map, explicitly unwrap sequence we’ve seen before.

Which means in RxSwift 5 you can do:let stringIsRequired: Observable<String> = optionalString .

compactMap { $0 }Yep.

With compactMap(), the equivalent to unwrap() is now effectively part of the RxSwift core.

So that’s it.

The addition of compactMap() to RxSwift 5 lets us write less code and is more performant and memory efficient to boot, replacing three RxSwift operations with just one.

And as an added bonus, we can now easily unwrap() our optional event streams without using another library or by adding the extension to our own code base.

Personally, I think it’s a useful addition to your RxSwift toolbox.

So useful, in fact, that like the variadic disposebag, I’m proud to note that compactMap was another of my minor contributions to the RxSwift project.

If you have any questions just leave ’em in the comments below.

Enjoy.

.

. More details

Leave a Reply