Adding a Radial Gravity Field to Your UI - UI Dynamics - iOS 9 Swift Programming Cookbook (2015)

iOS 9 Swift Programming Cookbook (2015)

Chapter 13. UI Dynamics

UI Dynamics allow you to create very nice effects on your UI components, such as gravity and collision detection. Let’s say that you have 2 buttons on the screen that the user can move around. You could create opposing gravity fields on them so that they repell each other and cannot be dragged into each other. Or for instance, you could provide a more live UI by creating a turbulence field under all your UI components so that they move around automatically ever so slightly (or through a noise field, as described in Recipe 13.4) even when the user is not interacting with themi. All of this is possible with the tools that Apple has given you in UIKit. You don’t have to use any other framework to dig into UI Dynamics.

One of the basic concepts in UI Dynamics is an animator. Animator objects, which are of type UIDynamicAnimator, hold every other effect together and orchestrate all the effects. For instance, if you have collision detection and gravity effects, the animator decides how the pull on an object through gravity will work hand in hand with the collision detection around the edges of your reference view.

Reference views are like canvases where all your animations happen. Effects are added to views and then added to an animator, which itself is placed on a reference view. In other words, the reference view is the canvas and the views on your UI (like buttons, lables, etc) will have effects.

13.1 Adding a Radial Gravity Field to Your UI

Problem

You want to add a radial gravity to your UI, with animations.

Solution

Use the radialGravityFieldWithPosition(_:) class method of UIFieldBehavior and add this behavior to a dynamic animator of type UIDynamicAnimator.

Discussion

A typical gravity behavior pulls items in a direction. A radial gravity field has a center and a region in which everything is drawn to the center, just like gravity on earth, whereby everything is pulled towards the core of this sphere.

For this recipe, I designed a UI like Figure 13-1. The gravity is at the center of the main view and the orange view is affected by it.

Figure 13-1. A main view and another view that is an orange square

The gravity field here is not linear. I would also like this gravity field to repel the orange view, instead of pulling it towards the core of gravity. Then I’d like the user to be able to pan this orange view around the screen and release it to see how the gravity affects the view at that point in time (think about pan gesture recognizers).

Let’s have a single view app that has no navigation bar and then go into IB and add a simple colorful view to your main view. I’ve created mine, colored it orange(ish), and have linked it to my view controller under the name orangeView (see Figure 13-2).

Figure 13-2. My view is added on top of the view controller’s view and hooked to the view controller’s code

Then from the object library, find a pan gesture recognizer (see Figure 13-3) and drop it onto your orange view so that it gets associated with that view. Find the pan gesture recognizer by typing its name into the object library’s search field.

Figure 13-3. Getting the pan gesture recognizer

Then associate the pan gesture recognizer’s code to a method in your code called panning(_:). So now your view controller’s header should look like this:

import UIKit

import SharedCode

class ViewController: UIViewController {

@IBOutlet var orangeView: UIView!

...

NOTE

Whenever I write a piece of code that I want to share between various projects, I put it inside a framework that I’ve written called SharedCode. You can find this framework in the GitHub repo of this book. In this example, I’ve extended CGSize so that I can find the CGPoint at the center of CGSize like so:

import Foundation

extension CGSize{

public var center: CGPoint{

return CGPoint(x: self.width / 2.0, y: self.height / 2.0)

}

}

Then in the vc, create your animator, specifying this view as the reference view:

lazy var animator: UIDynamicAnimator = {

let animator = UIDynamicAnimator(referenceView: self.view)

animator.debugEnabled = true

return animator

}()

If you are writing this code, you’ll notice that you’ll get a compiler error saying that the debugEnabled property is not available on an object of type UIDynamicAnimator. That is absolutely right. This is a debug only method that Apple has provided to us and which we should only use when debugging our apps. Because this property isn’t actually available in the header file of UIDynamicAnimator, we need to create a bridging header (with some small Objective-C code) to enable this property. Create your bridging header and then extend UIDynamicAnimator:

@import UIKit;

#if DEBUG

@interface UIDynamicAnimator (DebuggingOnly)

@property (nonatomic, getter=isDebugEnabled) BOOL debugEnabled;

@end

#endif

When the orange view is repelled by the reversed radial gravity field, it should collide with the edges of your view controller’s view and stay within the bounds of the view:

lazy var collision: UICollisionBehavior = {

let collision = UICollisionBehavior(items: [self.orangeView])

collision.translatesReferenceBoundsIntoBoundary = true

return collision

}()

Then create the radial gravity of type UIFieldBehavior. Two properties in this class are quite important:

region

This is of type UIRegion and specifies the region covered by this gravity.

strength

A floating point value that indicates (id positive) the force by which items get pulled into the gravity field. If you assign a negative value to this property, items get repelled by this gravity field.

In our example, I want the gravity field to consume an area with the radius of 200 points and I want it to repel items:

lazy var centerGravity: UIFieldBehavior = {

let centerGravity =

UIFieldBehavior.radialGravityFieldWithPosition(self.view.center)

centerGravity.addItem(self.orangeView)

centerGravity.region = UIRegion(radius: 200)

centerGravity.strength = -1 //repel items

return centerGravity

}()

When the user rotates the device, recenter the gravity:

override func viewWillTransitionToSize(size: CGSize,

withTransitionCoordinator

coordinator: UIViewControllerTransitionCoordinator) {

super.viewWillTransitionToSize(size,

withTransitionCoordinator: coordinator)

centerGravity.position = size.center

}

NOTE

Remember the center property that we just added on top of CGSize?

When our view is loaded, add your behaviors to the animator:

override func viewDidLoad() {

super.viewDidLoad()

animator.addBehavior(collision)

animator.addBehavior(centerGravity)

}

To handle the panning, consider a few things:

§ When panning begins, you have to disable your animators so that none of the behaviors have an effect on the orange view.

§ When the panning is in progress, you have to move the orange view where the user’s finger is pointing.

§ When the panning ends, you have to re-enable your behaviors.

All this is accomplished in the following code:

@IBAction func panning(sender: UIPanGestureRecognizer) {

switch sender.state{

case .Began:

collision.removeItem(orangeView)

centerGravity.removeItem(orangeView)

case .Changed:

orangeView.center = sender.locationInView(view)

case .Ended, .Cancelled:

collision.addItem(orangeView)

centerGravity.addItem(orangeView)

default: ()

}

}

See Also