Swift: UIView Animation Syntax Sugar

Because closures make ugly couples

If you hadn’t already heard, closures are a great tool to utilise in your Swift code. They’re first-class citizens, they can become trailing closures if they’re at the end of an API and now they’re @noescape by default which is a massive win in the fight against reference cycles.

But every once in a while we have to work with APIs that contain more than one closure, which turns this beautiful language feature into something far less appealing. I’m looking at you, UIView.

class func animate(withDuration duration: TimeInterval,            
animations: @escaping () -> Void,
completion: ((Bool) -> Void)? = nil)

Trailing Closure

UIView.animate(withDuration: 0.3, animations: {
// Animations
}) { finished in
// Compeleted
}

We’re mixing regular closures with trailing closures, animations: still has its parameter title but completion: loses its parameter title which makes it a trailing closure. I also feel that the trailing closure feels disconnected from the API in this type of context, but I guess this is because of the closing parentheses of the API and the inner closure followed by the opening parentheses:

}) { finished in // yuck
Note: If you’re unsure of what a trailing closure is, I have another article explaining what they are and how to use them. Swift: Syntax Cheat Codes

Indentation for readabilty

One could also argue against the default indentation for the animation closures because they’re both at the same level as the declaration. Lately I’ve been drinking the functional programming kool-aid big time, and one thing I absolutely love about writing functional code is how we list sequences of commandes in a bullet point format:

[0, 1, 2, 4, 5, 6]
.sorted { $0 < $1 }
.map { $0 * 2 }
.forEach { print($0) }

Why can’t double closure API’s act this way too?

Note: If you don’t understand the syntax of $0, I have another article explaining what they mean and how to use them. Swift: Syntax Cheat Codes

Forcing ugly to be beautiful

UIView.animate(withDuration: 0.3,
animations: {
// Animations
},
completion: { finished in
// Compeleted
})

I decided to take cues from functional programming syntax and make better use of indentations by fighting Xcode’s autocomplete and forcing my myself to layout UIView animation APIs like this. In my opinion it lays out code in a much more readable format than the previous but it’s also a labour of love. Each time you copy and paste this code, the indentation always messes up, but I guess that’s more of an Xcode problem than it is Swift, right?

Passing closures

let animations = {
// Animate
}
let completion = { (finished: Bool) in
// Completion
}
UIView.animate(withDuration: 0.3,
animations: animations,
completion: completion)

At the start of this post I mentioned that closures are first-class citizens in Swift-topia which means that we can assign them to variables and pass them around willingly. As valid as this code is, I don’t believe it reads as well as the previous example and I’m hesitant of the idea that other objects are can access and use these closures when they were intended for single purpose. If I was forced into an ultimatum, I would still choose the former.

The solution

As most programmers do, I forced myself into creating a solution for a relatively mundane problem under the promise to myself that it would 
“save time in the long run”.

UIView.Animator(duration: 0.3)
.animations {
// Animations
}
.completion { finished in
// Completion
}
.animate()

As you can see, the syntax and structure has been inspired by the things I’ve learned from using Swift’s functional programming APIs. We’ve traded in double closure API for a sequence of higher order functions and now our code reads a lot better and the compiler is fighting for us when we’re writing new lines and copy/pasting old ones.

“It will save time in the long run!”

Animator

class Animator {
typealias Animations = () -> Void
typealias Completion = (Bool) -> Void
    private var animations: Animations
private var completion: Completion?
private let duration: TimeInterval
    init(duration: TimeInterval) {
self.animations = {}
self.completion = nil
self.duration = duration
}
...

Our Animator type is a pretty simple one, it has three properties: a duration, two closures, an initialiser and some functions which we’ll get into shortly. We’ve used a couple typealias definitions to predefine the signature of our closures which isn’t necessary, but is always good practice to improve readability of our code and reduce error points when or if we decide to change the signature of our closure after we’ve implemented them in multiple places.

The closure properties are mutable because we need to store them somewhere and we intend the values to change after instantiation, but they’re also private because we want to avoid external mutation from happening. completion is optional to resemble the official UIView API whilst the animations is not. In our initialiser implementation, we’ve also defined default values to the closure properties so the compiler doesn’t complain.

func animations(_ animations: @escaping Animations) -> Self {
self.animations = animations
return self
}
func completion(_ completion: @escaping Completion) -> Self {
self.completion = completion
return self
}

The closure sequence implementations are incredibly simple, all they do is accept a specific closure argument and set its own corresponding closure value to the one which was passed in.

Returning Self

The cool thing is that these APIs return an instance of Self, which is the true magic here. Because we’re returning Self, we’re able to create the sequence-style API.

When we return Self on a function, it allows us to perform other functions on itself in the same execution:

let numbers = 
[0, 1, 2, 4, 5, 6] // Returns Array
.sorted { $0 < $1 } // Returns Array
.map { $0 * 2 } // Returns Array

However, if the last function in the sequence returns an object, it must be assigned to something so the compiler can do something with it, which is why we assigned it to the numbers constant.

If the last function returns Void then we don’t need to assign it to anythng for it to execute:

[0, 1, 2, 4, 5, 6]         // Returns Array
.sorted { $0 < $1 } // Returns Array
.map { $0 * 2 } // Returns Array
.forEach { print($0) } // Returns Void

Animating

func animate() {
UIView.animate(withDuration: duration,
animations: animations,
completion: completion)
}

Like a lot of my other ideas, all this neat stuff ends with a simple wrapper for a pre-existing API, but that’s not a bad thing, not at all. I’m a firm believer that Swift was created in a way that allows us as thinkers, tinkerers and programmers to reimagine and recraft the tools provided to us.

Extending UIView

extension UIView {
    class Animator { ...

Finally, we take our Animator class and place it within an extension of UIView, we’re doing for a couple of reasons. Firstly, we want the namespace of UIView so that it provides context to the API we just create and secondly, the functionality is directly related to UIView, which would make it pointless to have it as a standalone class.

Options

UIView.Animator(duration: 0.3, delay: 0, options: [.autoreverse])
UIView.SpringAnimator(duration: 0.3, delay: 0.2, damping: 0.2, velocity: 0.2, options: [.autoreverse, .curveEaseIn])

There are multiple options to choose from when working with the animation APIs, just check out the documentation. Through the power of default values in functions and class inhertiance, the Animator as well as the SpringAnimator classes now cover most of the types of animation APIs you would normally use.

As always I’ve provided a playground on GitHub for you to check out as well as a Gist incase you aren’t in front of Xcode.

If you like what you’ve read today you can check our my other articles, or if you plan on adopting this approach for your own project, please send me a tweet or follow me on Twitter, it really makes my day.


Announcement

I’m a big dork for Swift and the community is such an amazing one that I really wanted to contribute to its growth. I’m organising a conference in Melbourne on the 23rd and 24th of February, and it’s called Playgrounds.

All details can be found at www.PlaygroundsCon.com 
Follow us on Twitter:
@PlaygroundsCon