RxSwift : Introduction Benefits And its Uses
If you are an iOS developer who’s done some reasonable amount of UI work and is passionate
about it, you’ve got to love the power of UIKit when it comes to animations. Animating a UIView
is as easy as cake. You don’t have to think much about how to make it fade, rotate, move, or
shrink/expand over time. However, it gets a bit involved if you want to chain animations together
and set up dependencies between them. Your code may end up being quite verbose and hard
to follow, with many nested closures and indentation levels.
In this article, I’ll explore how to apply the power of a reactive framework such as RxSwift to make that code look much cleaner as well as easier to read and follow. The idea came to me when I was working on a project for a client. That particular client was very UI savvy (which perfectly matched my passion)! They wanted their app’s UI to behave in a very particular way, with a whole lot of very sleek transitions and animations. One of their ideas was to have an intro to the app which would tell the story of what the app was about. They wanted that story told through a sequence of animations rather than by playing a pre-rendered video so that it could be easily tweaked and tuned. RxSwift turned out to be a perfect choice for the problem like that, as I hope you’ll come to realize once you finish the article.
In this article, I’ll explore how to apply the power of a reactive framework such as RxSwift to make that code look much cleaner as well as easier to read and follow. The idea came to me when I was working on a project for a client. That particular client was very UI savvy (which perfectly matched my passion)! They wanted their app’s UI to behave in a very particular way, with a whole lot of very sleek transitions and animations. One of their ideas was to have an intro to the app which would tell the story of what the app was about. They wanted that story told through a sequence of animations rather than by playing a pre-rendered video so that it could be easily tweaked and tuned. RxSwift turned out to be a perfect choice for the problem like that, as I hope you’ll come to realize once you finish the article.
Short Intro to Reactive Programming
Reactive programming is becoming a staple and has been adopted in most of the modern
programming languages. There are plenty of books and blogs out there explaining in great
detail why reactive programming is such a powerful concept and how it helps with encouraging
good software design by enforcing certain design principles and patterns. It also gives you a
toolkit that may help you significantly reduce code clutter.
I’d like to touch on one aspect that I really like—the ease with which you can chain
asynchronous operations and express them in a declarative, easy-to-read way.
When it comes to Swift, there are two competing frameworks that help you turn it into a reactive programming language: ReactiveSwift and RxSwift. I will use RxSwift in my examples not because it’s better but because I am more familiar with it. I will assume that you, the reader, are familiar with it as well so that I can get directly to the meat of it.
When it comes to Swift, there are two competing frameworks that help you turn it into a reactive programming language: ReactiveSwift and RxSwift. I will use RxSwift in my examples not because it’s better but because I am more familiar with it. I will assume that you, the reader, are familiar with it as well so that I can get directly to the meat of it.
Chaining Animations: The Old Way
Let’s say you want to rotate a view 180° and then fade it out. You could make use of the
completion closure and do something like this
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
}, completion: { _ in
UIView.animate(withDuration: 0.5) {
self.animatableView.alpha = 0
}
})
self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
}, completion: { _ in
UIView.animate(withDuration: 0.5) {
self.animatableView.alpha = 0
}
})
It’s a bit bulky, but still okay. But what if you want to insert one more animation in between, say
shift the view to the right after it has rotated and before it fades away? Applying the same
approach, you will end up with something like this:
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
}, completion: { _ in
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0)
}, completion: { _ in
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.alpha = 0
})
})
})
self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
}, completion: { _ in
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0)
}, completion: { _ in
UIView.animate(withDuration: 0.5, animations: {
self.animatableView.alpha = 0
})
})
})
The more steps you add to it, the more staggered and cumbersome it gets. And then if you
decide to change the order of certain steps, you will have to perform some non-trivial cut and
paste sequence, which is error-prone.
Well, Apple has obviously thought of that—they offer a better way of doing this, using a keyframe-based animations API. With that approach, the code above could be rewritten like this:
UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [], animations: {
Well, Apple has obviously thought of that—they offer a better way of doing this, using a keyframe-based animations API. With that approach, the code above could be rewritten like this:
UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.33, animations: {
self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
})
UIView.addKeyframe(withRelativeStartTime: 0.33, relativeDuration: 0.33, animations: {
self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0)
})
UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.34, animations: {
self.animatableView.alpha = 0
})
})
self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2)
})
UIView.addKeyframe(withRelativeStartTime: 0.33, relativeDuration: 0.33, animations: {
self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0)
})
UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.34, animations: {
self.animatableView.alpha = 0
})
})
That’s a big improvement, with the key advantages being:
- The code stays flat regardless of how many steps you add to it
- Changing the order is straightforward (with one caveat below)
The disadvantage of this approach is that you have to think in terms of relative durations and it
becomes difficult (or at least not very straightforward) to change the absolute timing or order of
the steps. Just think about what calculations you would have to go through and what kind of
changes you would have to make to overall duration and relative durations/start times for each
of the animations if you decided to make the view fade within 1 second instead of half a second
while keeping everything else the same. Same goes if you want to change the order of steps—
you would have to recompute their relative start times.
Given the disadvantages, I don’t find any of the above approaches good enough. The ideal
solution I am looking for should satisfy the following criteria:
-
The code has to stay flat regardless of the number of steps
-
I should be able to easily add/remove or reorder animations and change their durations
independently without any side effects to other animations
I found that using, RxSwift, I can easily accomplish both of these goals. RxSwift is not the only
framework you could use to do something like that—any promise-based framework that lets you
wrap async operations into methods which can be syntactically chained together without making
use of completion blocks will do. But RxSwift has much more to offer with its array of operators,
which we will touch on a bit later.
Here is the outline of how I am going to do that:
- I will wrap each of the animations into a function that returns an observable of type Observable<Void>.
- That observable will emit just one element before completing the sequence.
- The element will be emitted as soon the animation wrapped by the function completes.
- I will chain these observables together using the flatMap operator.
This is how my functions can look like:
func rotate(_ view: UIView, duration: TimeInterval) -> Observable<Void> {
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
view.transform = CGAffineTransform(rotationAngle: .pi/2)
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
func shift(_ view: UIView, duration: TimeInterval) -> Observable<Void> {
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
view.frame = view.frame.offsetBy(dx: 50, dy: 0)
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
view.frame = view.frame.offsetBy(dx: 50, dy: 0)
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
func fade(_ view: UIView, duration: TimeInterval) -> Observable<Void> {
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
view.alpha = 0
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
view.alpha = 0
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
And here is how I put it all together:
rotate(animatableView, duration: 0.5)
.flatMap { [unowned self] in
self.shift(self.animatableView, duration: 0.5)
}
.flatMap { [unowned self] in
self.fade(self.animatableView, duration: 0.5)
}
.subscribe()
.disposed(by: disposeBag)
.flatMap { [unowned self] in
self.shift(self.animatableView, duration: 0.5)
}
.flatMap { [unowned self] in
self.fade(self.animatableView, duration: 0.5)
}
.subscribe()
.disposed(by: disposeBag)
It’s certainly much more code than in the previous implementations and may look like a bit of
overkill for such a simple sequence of animations, but the beauty is that it can be extended to
handle some pretty complex animation sequences and is very easy to read due to the
declarative nature of the syntax.
Once you get a handle on it, you can create animations as complex as a movie and have at your disposal a large variety of handy RxSwift operators that you can apply to accomplish things that would be very difficult to do with any of the aforementioned approaches.
Here is how we can use the .concat operator to make my code even more concise—the part where animations are getting chained together:
Once you get a handle on it, you can create animations as complex as a movie and have at your disposal a large variety of handy RxSwift operators that you can apply to accomplish things that would be very difficult to do with any of the aforementioned approaches.
Here is how we can use the .concat operator to make my code even more concise—the part where animations are getting chained together:
Observable.concat([
rotate(animatableView, duration: 0.5),
shift(animatableView, duration: 0.5),
fade(animatableView, duration: 0.5) ])
.subscribe()
.disposed(by: disposeBag)
rotate(animatableView, duration: 0.5),
shift(animatableView, duration: 0.5),
fade(animatableView, duration: 0.5) ])
.subscribe()
.disposed(by: disposeBag)
You can insert delays in between animations like this:
func delay(_ duration: TimeInterval) -> Observable<Void> {
return Observable.of(()).delay(duration, scheduler: MainScheduler.instance)
}
}
Observable.concat([
rotate(animatableView, duration: 0.5),
delay(0.5),
shift(animatableView, duration: 0.5),
delay(1),
fade(animatableView, duration: 0.5)
])
.subscribe()
.disposed(by: disposeBag)
rotate(animatableView, duration: 0.5),
delay(0.5),
shift(animatableView, duration: 0.5),
delay(1),
fade(animatableView, duration: 0.5)
])
.subscribe()
.disposed(by: disposeBag)
Now, let’s assume we want the view to rotate a certain number of times before it starts moving.
And we want to easily tweak how many times it should rotate.
First I’ll make a method that repeats rotation animation continuously and emits an element after each rotation. I want these rotations to stop as soon as the observable is disposed of. I could do something like this:
}
First I’ll make a method that repeats rotation animation continuously and emits an element after each rotation. I want these rotations to stop as soon as the observable is disposed of. I could do something like this:
func rotateEndlessly(_ view: UIView, duration: TimeInterval) -> Observable<Void> {
var disposed = false
return Observable.create { (observer) -> Disposable in
func animate() {
UIView.animate(withDuration: duration, animations: {
view.transform = view.transform.rotated(by: .pi/2)
}, completion: { (_) in
observer.onNext(())
if !disposed {
animate()
}
})
}
animate()
return Disposables.create {
disposed = true }
}
var disposed = false
return Observable.create { (observer) -> Disposable in
func animate() {
UIView.animate(withDuration: duration, animations: {
view.transform = view.transform.rotated(by: .pi/2)
}, completion: { (_) in
observer.onNext(())
if !disposed {
animate()
}
})
}
animate()
return Disposables.create {
disposed = true }
}
And then my beautiful chain of animations could look like this:
Observable.concat([
rotateEndlessly(animatableView, duration: 0.5).take(5),
shift(animatableView, duration: 0.5),
fade(animatableView, duration: 0.5)
])
.subscribe()
.disposed(by: disposeBag)
rotateEndlessly(animatableView, duration: 0.5).take(5),
shift(animatableView, duration: 0.5),
fade(animatableView, duration: 0.5)
])
.subscribe()
.disposed(by: disposeBag)
You see how easy it is to control how many times the view will rotate—just change the value
passed to the take operator.
Now, I’d like to take my implementation one step further by wrapping each if the animation
functions I created into the “Reactive” extension of UIView (accessible through the .rx suffix).
This would make it more along the lines of RxSwift conventions, where reactive functions are
usually accessed through the .rx suffix to make it clear that they are returning an observable.
extension Reactive where Base == UIView {
func shift(duration: TimeInterval) -> Observable<Void> {
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
self.base.frame = self.base.frame.offsetBy(dx: 50, dy: 0)
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
func fade(duration: TimeInterval) -> Observable<Void> {
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
self.base.alpha = 0
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
func rotateEndlessly(duration: TimeInterval) -> Observable<Void> {
var disposed = false
return Observable.create { (observer) -> Disposable in
func animate() {
func shift(duration: TimeInterval) -> Observable<Void> {
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
self.base.frame = self.base.frame.offsetBy(dx: 50, dy: 0)
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
func fade(duration: TimeInterval) -> Observable<Void> {
return Observable.create { (observer) -> Disposable in
UIView.animate(withDuration: duration, animations: {
self.base.alpha = 0
}, completion: { (_) in
observer.onNext(())
observer.onCompleted()
})
return Disposables.create()
}
}
func rotateEndlessly(duration: TimeInterval) -> Observable<Void> {
var disposed = false
return Observable.create { (observer) -> Disposable in
func animate() {
UIView.animate(withDuration: duration, animations: {
self.base.transform = self.base.transform.rotated(by: .pi/2)
}, completion: { (_) in
observer.onNext(())
if !disposed {
animate()
}
})
}
animate()
return Disposables.create {
disposed = true
}
}
}
}
self.base.transform = self.base.transform.rotated(by: .pi/2)
}, completion: { (_) in
observer.onNext(())
if !disposed {
animate()
}
})
}
animate()
return Disposables.create {
disposed = true
}
}
}
}
With that, I can put them together like this:
Observable.concat([
animatableView.rx.rotateEndlessly(duration: 0.5).take(5),
animatableView.rx.shift(duration: 0.5),
animatableView.rx.fade(duration: 0.5)
])
.subscribe()
.disposed(by: disposeBag)
animatableView.rx.rotateEndlessly(duration: 0.5).take(5),
animatableView.rx.shift(duration: 0.5),
animatableView.rx.fade(duration: 0.5)
])
.subscribe()
.disposed(by: disposeBag)
Conclusion :
As this Blog Post illustrates, by unleashing the power of RxSwift, and once you have your primitives in place, you can have real fun with animations. Your code is clean and easy to read, and it doesn’t look like “code” anymore.
As this Blog Post illustrates, by unleashing the power of RxSwift, and once you have your primitives in place, you can have real fun with animations. Your code is clean and easy to read, and it doesn’t look like “code” anymore.
Comments
Post a Comment