The Significance of Movement

Of late, I’ve been feeling very stagnant in life. The Significance of Movement hit me one morning when I started realizing how monotony and lack of motion can affect your state of being both mentally and physically. I was quickly able to relate that to how we have come to impart animation and movement digitally to convey ideas and present details to keep things exciting and fluid.

The Realism Paradigm

We strive for things to feel natural in the digital world. So, all natural physics based functions like gravity, collisions, and boundaries move our digital interfaces closer to reality making us feel more comfortable with it. UIKit Dynamics is pretty much an entire physics engine for UIKit which enables us to do these things. Subtle motion, mass to objects and a tiny tug-pull here and there make us feel closer to the interface because of the realism portrayed in it.

The UIKitty Way

UIKit comes bundled with UIKit Dynamics which assists us in delivering experiences that are closer to the natural world using physics. Let’s look at the fundamentals of UIKit dynamics and how to work with the basic classes that it offers.

UIKitDynamicAnimator

UIKitDynamicAnimator is the boss class that manages and orchestrates the animations in a view. It takes a referenceView where the animations will take place. It’s as simple as that.

Behaviors

Different kinds of behaviors can be added to an animator. Simply put, the behaviors added to an animator are respected by the UIKitDynamicAnimator. Behaviors generally are subclasses of UIDynamicBehavior and can be understood as physics that the world conforms to. Kinds of behaviors:

  • UIAttachmentBehavior
  • UICollisionBehavior
  • UIFieldBehavior
  • UIGravityBehavior
  • UIPushBehavior
  • UISnapBehavior

Let’s look at an example of the Gravity Behavior to understand how behaviors work. To understand gravity behavior, let’s build a simple square which will fall from where it’s placed due to gravity behavior.

To begin with, we would need an animator property in the View Controller class we’ll be implementing this animation:

var animator: UIDynamicAnimator!

In the viewDidLoad method, let’s initialize the animator with a reference view, in this case, we’ll use our view property since we’ll be performing all animations in the primary view of the view controller:

animator = UIDynamicAnimator(referenceView: view)

Now that we have an animator we could use to perform dynamic animations, we could go on to add behaviors to the animator. But before that, let’s add a box on which we will see how the gravity behavior works.

In the viewDidLoad method, let’s go ahead and add a 2D box:

let box = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
box.backgroundColor = .gray
view.addSubview(box)

You should now have a box that looks like this:

Now we have it all set up, we could understand behaviors by adding one. Since the View Controller has animations, it’s a good practice to declare the animators and behaviors in a single place on the top level scope. Let’s go ahead an declare a gravity behavior next to our animator:

var animator: UIDynamicAnimator!
var gravityBehavior: UIGravityBehavior!

It’s important to understand that Behaviors are bound to items and can be added to an animator. Items are objects that implement the UIDynamicItem protocol. The UIView and UICollectionViewLayoutAttributes implement this protocol already, so simply by using UIViews as our items, we are able to bind it to behaviors.

Since a behaviour is bound to items, while initialising a behaviour we pass in an array of items that will respect this behaviour. Let’s do that after our box is initialised in viewDidLoad:

// Setup a Box
let box = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))`
box.backgroundColor = .gray
view.addSubview(box)

// Initialize Gravity Behavior
gravityBehavior = UIGravityBehavior(items: [box])

What happens here is quite straightforward. Now that we have set up our animator and a behavior. All there’s left to do is binding this behavior to our animator:

animator.addBehavior(gravity)

Doing this, we see how the box starts to fall down because of the gravity behavior that we’ve just added.

Collisions and Boundaries

We now see how the gravity behavior makes the square fall indefinitely. It would be nice if it could consider the bottom of the screen as a floor, wouldn’t it?

Collision Behaviors help us define how objects interact with each other. This kind of sounds like what we need at the moment. By defining a collision behavior, we could define a boundary around each item the object is associated with.

In our case, we would need to define our boundary around the box that’s falling indefinitely. Boundaries are defined using a path, UIBezierPath to be specific, using this method:

collision.addBoundaryWithIdentifier(forPath:)

Let’s begin by defining a collision behavior:

var animator: UIDynamicAnimator!
var gravityBehavior: UIGravityBehavior!
var collisionBehavior: UICollisionBehavior!

Since our box is going to function inside of our reference view boundaries, there’s a handy property on collision behavior object called translatesReferenceBoundsIntoBoundary which when set to true, takes care of setting our view’s bounds as the boundary for the behavior. Handy, isn’t it?

Let’s do just that:

// Setup a Box
let box = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
box.backgroundColor = .gray
view.addSubview(box)

// Initialize Gravity Behavior`
gravityBehavior = UIGravityBehavior(items: [box])

// Collision`
collisionBehavior = UICollisionBehavior(items: [box])
collisionBehavior.translatesReferenceBoundsIntoBoundary = true

animator.addBehavior(gravityBehavior)
animator.addBehavior(collisionBehavior)

This would give us something like this:

There are a lot more behaviors that we could take advantage of to write intuitive and exciting apps.

What’s the fun in making a box fall, you ask? Say no more. Let’s put what we’ve learned to good use by building a UI.

Swiping like a Dev

What we’ll be building:

There are two parts to building this UI:

  • Build a Draggable Card View
  • Make it snap back to its position

Building a Draggable Card View

Building a draggable card is just a matter of adding a UIView, and implementing a pan gesture recogniser. Assuming we’ve already setup our view (lets call it card) in the storyboard and hooked it up to the View Controller. We can add and implement a Pan Gesture Recognizer:

let panGesture = UIPanGestureRecognizer(target: self, action: #selector(pan))
card.addGestureRecognizer(panGesture)
card.isUserInteractionEnabled = true

@objc func pan(recognizer: UIPanGestureRecognizer) {
	switch recognizer.state {
	case .changed:
	  let translation = recognizer.translation(in: view)
	  card.center = CGPoint(x: card.center.x + translation.x,
								y: card.center.y + translation.y)
	  recognizer.setTranslation(.zero, in: view)
	default: break
	}
  }

Here we use a pan gesture to implement dragging on the card, by getting the current translation in view and using the translation to set the card’s position as the finger is dragged. The pan(recognizer:) is called continuously with the state .changed when the finger is dragged across the screen which changes the card’s position continuously.

We would get something like this:

Make it Snap

Now that we have a draggable view, let’s move on to building the snap behavior. We’ll use the UISnapBehavior class to implement this.

The two essential things out of our UIKit Dynamics backpack are:

  • Animator
  • Behavior

To begin with, we will declare an animator property like earlier in the class and initialize it with the primary view in the viewDidLoad method.

Next comes the snap behavior, The UISnapBehavior class takes in two parameters when instantiating an object:

  • item - This is the item which it acts on, in our case, it will be card
  • snapTo - This is a CGPoint parameter, which defines where the item will snap to when it’s dragged, in our case, it will be view.center since we would like it to snap back to the view’s center

Initializing the snap behavior and adding it to the animator would be a straightforward process since we’ve gone through something similar already:

var animator: UIDynamicAnimator!
var snapBehavior: UISnapBehavior!

@IBOutlet weak var card: UIView!

override func viewDidLoad() {
super.viewDidLoad()
animator = UIDynamicAnimator(referenceView: view)
snapBehavior = UISnapBehavior(item: card, snapTo: view.center)
animator.addBehavior(snapBehavior)

But this alone will not make the snap behavior work, this is because even though our snap behavior is in place, the pan gesture recognizer takes over moving the card just like before with no regard to the behavior. To be able to fix this, we need to remove the behavior on dragging and re-add it when the gesture has ended. This can be done easily thanks to recognizer states:

@objc func pan(recognizer: UIPanGestureRecognizer) {
    switch recognizer.state {
        case .began:
          animator.removeBehavior(snapBehavior)
        case .changed:
          let translation = recognizer.translation(in: view)
          cardView.center = CGPoint(x: cardView.center.x + translation.x,
                                    y: cardView.center.y + translation.y)
          recognizer.setTranslation(.zero, in: view)
        case .ended, .cancelled, .failed:
          animator.addBehavior(snapBehavior)
        default:
          break
    }
}

Now this should give us the snap behaviour we’re expecting:

This is a primer to get someone with no prior experience in UIKit Dynamics off the ground and doesn’t cover all of what UIKit Dynamics is capable of. I highly recommend going through Apple’s Developer documentation on the subject and playing with all kinds of behaviors to fully appreciate what UIKit Dynamics can do.

All kinds of feedback and questions are welcome.

Thanks to Raul's article on UIKit Dynamics where his approach to explaining UISnapBehavior is on point, which I have borrowed.