A better API for UIView Animations
After reading Andyy Hope’s article about UIView syntactic sugar, I was inspired to give my own interpretation of the technique.
The main idea is to turn the existing api…
UIView.animate(withDuration: 0.3, animations: {
// Animations
}) { finished in
// Completed
}
Into this…
UIView.animate(withDuration: 0.3)
.animations {
// Animations
}
.completion { finished in
// Completed
}
It is a small change, but the way it looks is much more pleasing to the eye.
How did Andyy Hope accomplish this?
When I read that he was giving a functional make-over, I instantly thought of currying and made this.
var syntacticSugar = { (duration: TimeInterval) in
{ (animations: @escaping () -> ()) in
{ (completion: ((Bool) -> ())?) in
UIView.animate(withDuration: duration, animations: animations, completion: completion)
}
}
}// Can be called like this
syntacticSugar(10)({
// animation code
})({ _ in
// completion code
})
You may have noticed that I did not include the return type in these closures. When you write it like this the swift language can infer the type. It can be difficult to get the type information correct especially when it looks something like this…
((@escaping () -> ()) -> (((Bool) -> ())?) -> ())
Yikes! What type is that? How many parenthesis are there?
Anyways the currying did not seem to allow flexibility so I initially abandoned it only to come back to it later.
I placed my focus on the main technique at hand: Using Self as the return type. I attempted to use protocol extensions to fill out the necessary implementation details. I was going for a Class that would transform into different protocol types. I had one type for each api.
One for animate(withDuration:animation:completion)
One for animate(withDuration:delay:options:animations:completion:)
One for animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)
One for animateKeyframes(withDuration:delay:options:animations:completion:)
My intention was for all animations to start as animate(withDuration:animation:completion) but upon adding a value unique to each animation it would transform to the specialized type of animation call (i.e. setting UIViewAnimationOptions)
Although this worked, it was complex and did not scale well for large apis.
I eventually circled back to using currying to get the desired API. Here is what I did…
extension UIKit.UIView {
public struct Animator {
fileprivate var animatorBlock: ((@escaping () -> (), @escaping (Bool) -> ()) -> ())
fileprivate var animations: () -> () = { _ in }
public init(animatorBlock: @escaping ((@escaping () -> (), @escaping (Bool) -> ()) -> ())) {
self.animatorBlock = animatorBlock
}
public func animations(_ animations: @escaping () -> ()) -> Animator {
var a = self
a.animations = animations
return a
}
public func completion(_ completion: @escaping (Bool) -> ()) {
animatorBlock(animations, completion)
}
}
public static func animate(withDuration duration: TimeInterval) -> Animator {
return Animator(animatorBlock: {
UIView.animate(withDuration: duration, animations: $0, completion: $1)
})
}
}
I had to dilute the curry so it wouldn’t be so spicy. I curry the animation and completion parameters and store them in an Animator struct as a type ((() -> (), (Bool) -> ()) -> ())
This means that all I have to do to fire the animation is to fill in the arguments for the animation and completion.
This means that I can modify the animation block before finally starting the animation. I made a design choice to start the animation as soon as the completion block is called.
This new API allows me to write code like this:
UIView.animate(withDuration: 5)
.animations {
// code
}
.completion { _ in
// code
}
This is a nice technique and I anticipate using it again in the near future.
The full implementation of this can be found on github. Go there to see how I applied the technique to the other UIView animation api’s
animate(withDuration:delay:options:animations:completion:)animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)animateKeyframes(withDuration:delay:options:animations:completion:)