This post originally appeared on Medium.
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
// Compelted
}
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 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 < $0 }
.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 < $0 } // 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 < $0 } // 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 playgrounds 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.
Playgrounds is Australia’s conference dedicated to Swift and Apple platform developers. It’s happening February 23rd & 24th, 2016, in Melbourne. Learn more on their website and follow along on Twitter @playgroundscon.
Receive news and updates from Realm straight to your inbox