I love good RxSwift articles, but I’m afraid I have a couple of issues with this one.
I realize you’re illustrating chaining asynchronous observables, but the first example doesn’t really make much sense in that there’s no asynchronous aspect to it and a simple inline filter would both eliminate the flatmap and the following filter.
private func bindTextField() {
textField.rx.text
.orEmpty
.filter { $0.count == 5 }
.filter { $0.lowercased().contains("rx") }
.flatMap { _ in self.animateTextField() }
.filter { $0 == true }
.subscribe(onNext: { _ in
print("Subscribed")
})
.disposed(by: disposeBag)
}
It might seem like I’m nitpicking, but my basic problem with the first example chosen is that it makes it seem as if Rx requires a lot of code to do something that’s simple to do otherwise, when in fact it does not.
Second problem is that there’s a retain cycle inside the flatmaps where they both capture self. I also realize that sometimes we want to simplify out examples for clarity, but retain cycles are a particular RxSwift bugaboo and we really should demonstrate best practices. So…
private func bindTextField() {
textField.rx.text
.orEmpty
.filter { $0.count == 5 }
.filter { $0.lowercased().contains("rx") }
.flatMap { [weak self] _ in
self?.animateTextField()
}
.subscribe(onNext: { _ in
print("Subscribed")
})
.disposed(by: disposeBag)
}
The result of the flatmap is now optional, but as our subscription is ignoring the value we could also do away with the following filter as well.
The final point is more insidious, but basically comes from the fact that your asynchronous animateTextField function never sends a completion event. As such, flatmap will subscribe to the observable created the first time through the sequence. It will then subscribe to the second observable created the second time through the sequence… keeping the first one as well and giving us TWO subscriptions now being maintained.
In other words, since neither subscription completed flatmap needs to keep an eye on both of them in case another event comes down the pipe. It won’t, but it doesn’t know that.
Each time through the flatmap we’re going to be adding another subscription to the list and in effect we’re going to be leaking subscriptions until the VC terminates.
The solution is to…
- Send onCompleted after the onNext from your animateTextField function.
- Use a Single, which will automatically complete.
- Use flatMapLatest, which will unsubscribe from the previous subscription each time through the sequence.
As shown…
private func bindTextField() {
textField.rx.text
.orEmpty
.filter { $0.count == 5 }
.filter { $0.lowercased().contains("rx") }
.flatMapLatest { [weak self] _ in
self?.animateTextField()
}
.subscribe(onNext: { _ in
print("Subscribed")
})
.disposed(by: disposeBag)
}
Flatmap takes some getting used to, but as you pointed out it’s a powerful concept once you get a handle on it.