RxSwift and Animations in iOS

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.

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.

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
            }
        })

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
                })
            })
        })

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: {
            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
            })
        })

That’s a big improvement, with the key advantages being:

1.      The code stays flat regardless of how many steps you add to it

2.      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:

1.      The code has to stay flat regardless of the number of steps

2.      I should be able to easily add/remove or reorder animations and change their durations independently without any side effects to other animations

Chaining Animations: The RxSwift Way

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:

1.      I will wrap each of the animations into a function that returns an observable of type Observable<Void>.

2.      That observable will emit just one element before completing the sequence.

3.      The element will be emitted as soon the animation wrapped by the function completes.

4.      I will chain these observables together using the flatMapoperator.

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()
        }
    }
 
    func fade(_ view: UIView, duration: TimeInterval) -> Observable<Void> {
        return Observable.create { (observer) -> Disposable in
            UIView.animate(withDuration: duration, animations: {
                view.alpha = 
            }, 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)

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:

        Observable.concat([
                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)

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:

    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 = 
            }
        }
    }

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)

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 .rxsuffix 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() {
                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 = 
            }
        }
    }
}

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)

Related Posts

Comments are closed.

© 2024 Software Engineering - Theme by WPEnjoy · Powered by WordPress