Making your UI Elements to Shake with Shakeable Protocol

Ashwin Shrestha
6 min readMay 3, 2020

--

In this article, we will be making a swift protocol which when conformed by any class which inherits UIView, can shake along X-axis or Y-axis. For this we are going to add CABasicAnimation, to the layer of the desired view, thus making it shake.

This is Part 1, of a series Swift Protocols and adding those nifty functionalities in your app using them. The base article of the series can be found here :

https://medium.com/@ashwinshres/swift-protocols-and-adding-those-nifty-functionalities-in-your-app-using-them-50a9680891ef

Please go through it, before you start with this article.

Starting with the core first:

I won’t bore you with the basics of UIView as there is a plethora of very good articles and tutorials about it. However, would like to give a quick outline of CALayer.

From Apple documentation:

A CALayer is an object that manages image-based content and allows you to perform animations on that content.

Every UIView is just a wrapper around CALayer, it’s the root CALayer object of a view that is used to render the view It can be accessed by calling the layer property. While coding we often use:

open class func animate(withDuration duration: TimeInterval, delay: TimeInterval, options: UIView.AnimationOptions = [], animations: @escaping () -> Void, completion: ((Bool) -> Void)? = nil)

to animate a view property like: frame, backgroundColor, alpha, transform and constraints. But we can animate a view, by animating it’s CALayer as well. So What’s the difference between UIView animation and CALayer animation?

Animating a frame of a UIView can be a daunting task for CPU. Even if we add UIView animations, in the end, all animations are converted to Core Animation-style animations; that is, everything is animated using Core Animation. Here is a nice answer posted in Stack overflow on the same question: https://stackoverflow.com/a/38965402

From Apple documentation:

Core Animation provides high frame rates and smooth animations without burdening the CPU and slowing down your app. Most of the work required to draw each frame of an animation is done for you. You configure animation parameters such as the start and end points, and Core Animation does the rest, handing off most of the work to dedicated graphics hardware, to accelerate rendering.

Coming back to the topic of our article, here we are trying to shake a UIView along X-axis on certain conditions met, or when a user takes an action. To give it a case scenario, let’s suppose, we want to shake a UITextField along X-axis when a user enters invalid data. Shaking any UIView i.e. we are actually adding animation to its root layer, means we are trying to animate its position. Let’s set up our animation first:

let animation = CABasicAnimation(keyPath: "position")

Here in the above code sample, we have created a new CABasicAnimation with it’s keyPath “position”. CABasicAniamtion can have following items as its key path: position, opacity, backgroundColor, transform(transform.scale.x) . More on this can be found here

From Apple’s documentation:

CABasicAnimation

An object that provides basic, single-keyframe animation capabilities for a layer property.

Next, is how long do we want to animate i.e. shake the textfield along xAxis in seconds

animation.duration = 0.05

How many times do we want to repeat the animation:

animation.repeatCount = 5

After changing the position, do we want to play the animation in reverse?

animation.autoreverses = true // it's false by default

Now we need to provide the start and end position of the animation:

let distance: CGFloat = 5.0animation.fromValue =  NSValue(cgPoint: CGPoint(x: center.x - distance, y: center.y))animation.toValue =  NSValue(cgPoint: CGPoint(x: center.x + distance, y: center.y))

Here, we have animated the position of the layer from the layer’s center.x minus a distance value of 5.0 to center.x adding a distance value of 5.0

And then added the animation to the layer:

layer.add(animation, forKey: "position")

The full code snippet for above is:

func shakeAlongXAxis() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = duration
animation.repeatCount = repeatcount
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: center.x - distance, y: center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: center.x + distance, y: center.y))
layer.add(animation, forKey: "position")
}

Now let’s make a Protocol limiting adoption to UIView

protocol ShakeableX where Self: UIView {
var duration: Double {set get}
var repeatcount: Float {set get}
var distance: CGFloat {set get}
func shakeAlongXAxis()
}
extension ShakeableX {

var duration: Double {
set { duration = newValue }
get { return AnimatableConstants.shakeDuration }
}
var repeatcount: Float {
set { repeatcount = newValue }
get { return AnimatableConstants.shakeCount }
}
var distance: CGFloat {
set { distance = newValue }
get { return AnimatableConstants.shakeDistance }
}
func shakeAlongXAxis() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = duration
animation.repeatCount = repeatcount
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: center.x - distance, y: center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: center.x + distance, y: center.y))
layer.add(animation, forKey: "position")
}
}

Here we have ShakeableX protocol with default implementation provided in the extension as for any UIVIew or its subclasses like UIButton, UIImageView . The reason we added a default implementation rather than implementing the method in the class which conforms this protocol is because for all UIView and subclasses, the implementation would be same in our example.

Do go through this article : https://medium.com/@ashwinshres/protocol-and-limiting-protocol-adoption-f211d05e45d8. for more info on this.

Also, we have made a struct with default value of our different constants / variables for the purpose of this article series.

struct AnimatableConstants {
static let shakeCount: Float = 5
static let shakeDuration: Double = 0.05
static let shakeDistance: CGFloat = 5
}

That’s all, now we can just conform this protocol to any UIView or it’s subclass and call shakeAlongXAxis() method to shake the view.

class AppTextField: UITextField, ShakeableX {    func shakeIfInputIsEmpty() {
if (text ?? "").isEmpty {
shakeAlongXAxis()
}
}
}

If we want to change the shakeCount, shakeDuration, shakeDistance, we can just provide those values here like:

class AppTextField: UITextField, ShakeableX {    var duration: Double = 0.5
var distance: CGFloat = 10
var repeatcount: Float = 10
func shakeIfInputIsEmpty() {
if (text ?? "").isEmpty {
shakeAlongXAxis()
}
}
}

So this is much the gist of how we can make a protocol and conform it to Shake things along x-axis. In the code below, we have made a Shakeable protocol, which is inherited by both ShakeableX and ShakeableY, to shake things along both or either X-axis or Y-axis

protocol Shakeable {
var duration: Double { set get }
var repeatcount: Float { set get }
var distance: CGFloat { set get }
}
protocol ShakeableX: UIView, Shakeable {
func shakeAlongXAxis()
}
extension ShakeableX { func shakeAlongXAxis() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = duration
animation.repeatCount = repeatcount
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: center.x - distance, y: center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: center.x + distance, y: center.y))
layer.add(animation, forKey: "position")
}
}protocol ShakeableY: UIView, Shakeable {
func shakeAlongYAxis()
}
extension ShakeableY { func shakeAlongYAxis() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = duration
animation.repeatCount = repeatcount
animation.autoreverses = true
animation.fromValue = NSValue(cgPoint: CGPoint(x: center.x, y: center.y - distance))
animation.toValue = NSValue(cgPoint: CGPoint(x: center.x, y: center.y + distance))
layer.add(animation, forKey: "position")
}
}
class AppTextField: UITextField, ShakeableX, ShakeableY {
var duration: Double = 0.0
var repeatcount: Float = 0.0
var distance: CGFloat = 0.0
func shakeIfInputIEmpty() {
if (text ?? "").isEmpty {
shakeAlongXAxis()
shakeAlongYAxis()
}
}
}

Note: when we add both shakeAlongXAxis() and shakeAlongYAxis() , the first one gets cancelled as we add a new CABasicAnimation on the layer. Adding both x-axis and y-axis shaking simultaneously requires a few code modification, which I am leaving for some other time 😀.

In the next article,

https://medium.com/@ashwinshres/making-your-ui-elements-to-bounce-with-bouncable-protocol-6d6c622eb63a

we will be adding a Bouncable functionality with protocol to a UIView or any class which inherits UIView

If you have any questions or suggestions, feel free to post them in the comment section.

Thanks for reading.

Happy Coding 🙂

--

--

Ashwin Shrestha
Ashwin Shrestha

Written by Ashwin Shrestha

Software Engineer who loves to code

No responses yet