Collision Detection with UIKit Dynamics iOS Tutorial

With UIKit Dynamics you can specify collision behaviours to your objects. The dynamic items can collide with each other and/or any boundary you specify. In this tutorial we will create some custom boundaries and randomly let some squares fall down on to these boundaries. This tutorial is made with Xcode 8.3 and built for iOS 10.3

Project Setup

Open Xcode and create a new Single View Application.

For product name, use iOS10CollisionDectectionTutorial and then fill out the Organization Name and Organization Identifier with your customary values. Enter Swift as Language and make sure only iPhone is selected in Devices.

To draw some lines a custom UIView will be used, where the drawRect method will be altered.. Select File -> New -> File -> iOS -> Source -> Cocoa Touch Class. Name the class LineView and make it a subclass of UIView. 

Go to the LineView.swift file.To draw the lines we will create a helper method drawLineFromPoint(fromX:toPoint:pointY:)

func drawLineFromPoint(fromX: CGFloat, toPoint toX: CGFloat, pointY y: CGFloat) {
    let currentContext = UIGraphicsGetCurrentContext()
        
    if let currentContext = currentContext {
        currentContext.setLineWidth(5.0)
        currentContext.move(to: CGPoint(x: fromX, y: y))
        currentContext.addLine(to: CGPoint(x: toX, y: y))
        currentContext.strokePath();
    }}

The lines will be drawn with a width of 5 points. Next, we will alter the drawRect method

override func draw(_ rect: CGRect) {
        
    drawLineFromPoint(fromX: 0, toPoint: bounds.size.width/3, pointY: bounds.size.height - 100.0)
    drawLineFromPoint(fromX: bounds.size.width/3, toPoint:bounds.size.width*0.67, pointY:bounds.size.height - 150.0)
    drawLineFromPoint(fromX: bounds.size.width*0.67, toPoint:bounds.size.width, pointY:bounds.size.height - 100.0)}

We make three calls to our helper function creating three lines of the same width with different heights. We need to connect our MyView to the view controller. Go to the storyboard and select the view. Go to the Identity Inspector and change the class to MyView.

Build and Run the project, the Lines are drawn on the screen.

Next, drag a button to the Storyboard and give it a title of "Next". Select the Buttonl and select the Auto Layout align button. Select the "Horizontally in Container" checkbox and click "Add 1 Constraint".

Select the Button and select the Auto Layout pin button. Pin the label to the top and and click "Add 1 Constraint".

The main view should look like this.

Select the Assistant Editor and make sure the ViewController.swift is visible. Ctrl and drag from the Next Button to the ViewController class  and create the following Action.

 

In ViewController.swift some properties needs to be declared to keep track of the views. Add the following properties

var squareViews:[UIView] = []
var animator:UIDynamicAnimator!
var colors:[UIColor] = []
var centerPoint:[CGPoint] = []
var sizeOfSquare:CGSize!

The squareViews propery will contain our views. We need the colors array, the centerPoint array and the sizeOfSquare property  to allocate them to our views later on. The animator property is needed to dynamically allocate our behaviours later. Next add the following properties

var leftBoundaryHeight:CGFloat!
var middleBoundaryHeight:CGFloat!
var rightBoundaryHeight:CGFloat!
var leftBoundaryWidth:CGFloat!
var middleBoundaryWidth:CGFloat!
var leftSquareCenterPointX:CGFloat!
var middleSquareCenterPointX:CGFloat!
var rightSquareCenterPointX:CGFloat!
var squareCenterPointY:CGFloat!

These properties are needed to set our custom boundaries and add a starting point for our squares. First, create a helper method setBoundaryValues to set these properties.

func setBoundaryValues() {
    leftBoundaryHeight = view.bounds.size.height - 100.0
    middleBoundaryHeight = view.bounds.size.height - 150.0
    rightBoundaryHeight = view.bounds.size.height - 100.0
    leftBoundaryWidth = view.bounds.size.width/3
    middleBoundaryWidth = view.bounds.size.width * 0.67
    leftSquareCenterPointX = view.bounds.size.width/6
    middleSquareCenterPointX = view.bounds.size.width/2
    rightSquareCenterPointX = view.bounds.size.width * 0.84
    squareCenterPointY = view.bounds.size.height - 400
}

In viewDidLoad, we will call our helper method and set the remainder of the square values.

override func viewDidLoad() {
    super.viewDidLoad()
        
    setBoundaryValues()
            
    // Create the colors array
    colors = [UIColor.red, UIColor.blue, UIColor.green, UIColor.purple, UIColor.gray]
            
    // Create the centerpoint of our squares
    let leftCenterPoint = CGPoint(x: leftSquareCenterPointX, y: squareCenterPointY)
    let middleCenterPoint = CGPoint(x: middleSquareCenterPointX, y: squareCenterPointY)
    let rightCenterPoint = CGPoint(x:rightSquareCenterPointX, y: squareCenterPointY)
    centerPoint = [leftCenterPoint,middleCenterPoint,rightCenterPoint]
            
    // set the size of our squares
    sizeOfSquare = CGSize(width: 50.0, height: 50.0) 
}

So, every view has a size of 50 and can be of 5 different colors. The main flow of our project happens in our releaseNextSquare(sender:) IBAction method. 

@IBAction func releaseSquare(_ sender: Any) {
    let newView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: sizeOfSquare.width, height: sizeOfSquare.height))
        
    let randomColorIndex = Int(arc4random()%5)
    newView.backgroundColor = colors[randomColorIndex]
        
    let randomCenterPoint = Int(arc4random()%3)
    newView.center = centerPoint[randomCenterPoint]
        
    squareViews.append(newView)
    view.addSubview(newView)
}

The view is created and a random centerpoint and color is assigned. The new view is added to the main view and to our array. Add the remainder of the lines to the end of the releaseSquare(sender:) action method

animator = UIDynamicAnimator(referenceView: view)

// create gravity
let gravity = UIGravityBehavior(items: squareViews)
animator.addBehavior(gravity)

// create collision detection
let collision = UICollisionBehavior(items: squareViews)
        
// set collision boundaries
collision.addBoundary(withIdentifier: "leftBoundary" as NSCopying, from: CGPoint(x: 0.0,y: leftBoundaryHeight), to: CGPoint(x: leftBoundaryWidth, y: leftBoundaryHeight))
collision.addBoundary(withIdentifier: "middleBoundary" as NSCopying, from: CGPoint(x: view.bounds.size.width/3,y: middleBoundaryHeight), to: CGPoint(x: middleBoundaryWidth, y: middleBoundaryHeight))
collision.addBoundary(withIdentifier: "rightBoundary" as NSCopying, from: CGPoint(x: middleBoundaryWidth,y: rightBoundaryHeight), to: CGPoint(x: view.bounds.size.width, y: rightBoundaryHeight))
        
collision.collisionMode = .everything
animator.addBehavior(collision)

First, we add a gravity behavior to our animator to let our squares fall. Next, a collision behaviour is added with our custom boundaries. This behaviour is also added to our animator. The default collision mode of UICollisionBehaviour is UICollisionBehaviourMode.everything, which means that all items and boundary can collide with each other. Build and Run, and keep pressing the Next button to let the squares fall down.

You can download the source code of the iOS10CollisionDectectionTutorial at the ioscreator repository on Github.