Mobile8 minute read

RxSwift and Animations in iOS

iOS developers love the power of UIKit, and animating a UIView is usually fairly easy. However, if you want to chain animations together and set up dependencies between them, your code can quickly become difficult to read 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. My client wanted a story told through a sequence of animations rather than by playing a pre-rendered video, and with RxSwift, we easily tweaked it to perfection.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

iOS developers love the power of UIKit, and animating a UIView is usually fairly easy. However, if you want to chain animations together and set up dependencies between them, your code can quickly become difficult to read 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. My client wanted a story told through a sequence of animations rather than by playing a pre-rendered video, and with RxSwift, we easily tweaked it to perfection.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Vadim Dagman
Verified Expert in Engineering

Vadim is a seasoned freelance developer, architect, technical manager, and entrepreneur with 25+ years of experience.

Read More

Expertise

PREVIOUSLY AT

Mediashower
Share

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

Animated GIF of a rotating square

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

Animated GIF of the rectangle rotating, pausing, and then sliding to the right

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

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

You see how easy it is to control how many times the view will rotate—just change the value passed to the take operator.

Animated GIF of the improved animation

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

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)

Where to Go from Here

As this article 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—you “describe” how your animations are put together and they just come alive! If you want to do something more elaborate than described here, you can certainly do that by adding more primitives of your own encapsulating other types of animations. You can also always take advantage of the ever-growing arsenal of tools and frameworks developed by some passionate people in the open source community.

If you are an RxSwift convert, keep visiting the RxSwiftCommunity repository regularly: you can always find something new!

If you’re looking for more UI advice, try reading How to Implement a Pixel-perfect iOS UI Design by fellow Toptaler Roman Stetsenko.


Source Notes (Understanding the Basics)

Understanding the basics

  • What is Swift used for?

    “Swift is a general-purpose, multi-paradigm, compiled programming language developed by Apple Inc. for iOS, macOS, watchOS, tvOS, Linux and z/OS. Swift is designed to work with Apple’s Cocoa and Cocoa Touch frameworks and the large body of existing Objective-C code written for Apple products.”

  • What is UIVIew in iOS?

    “The UIView class defines a rectangular area on the screen on which it shows content. It handles the rendering of any content in its area and also any interactions with that content – touches and gestures.”

  • What is meant by reactive programming?

    Reactive programming is an asynchronous programming paradigm concerned with asynchronous data streams and the propagation of change.

  • What is RxSwift?

    RxSwift is the reactive programming library for iOS. It makes it easy to program dynamic apps that respond to data changes and user events.

  • What is an observable?

    Observables provide support for passing messages between publishers and subscribers in your application. Observables convert data streams into sequence of events delivered to one or more subscribers asynchronously.

Hire a Toptal expert on this topic.
Hire Now
Vadim Dagman

Vadim Dagman

Verified Expert in Engineering

San Francisco, CA, United States

Member since August 8, 2013

About the author

Vadim is a seasoned freelance developer, architect, technical manager, and entrepreneur with 25+ years of experience.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

PREVIOUSLY AT

Mediashower

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.