UIDynamicAnimator stops reacting to gravity changes

504 views Asked by At

I got the exact same problem as stated in https://stackoverflow.com/questions/28275004/uidynamicanimator-stop-reacting-to-uigravitybehavior. I can't really explain it any better either.

Does anyone know why the UIDynamicAnimator would suddenly stop animating objects/items that have the UIGravityBehavior attached to them?

EDIT

I'm running the example from the Big Nerd Ranch

I modified the example so there are only two cubes, to make reproducing the error easier. The cubes are falling down and are reacting to gravity just fine, but if you tilt the phone so that the cubes end up in the corner, touching each other, then it stops any motion, thus not reacting to the gravity behavior anymore.

I'm also wondering, if this is a Swift issue. Maybe I should try to implement this in Obj-C to see if the error persists.

Here is the example:

//
//  ViewController.swift
//  Rock Box
//
//  Created by Steve Sparks on 7/11/14.
//  Copyright (c) 2014 Big Nerd Ranch. All rights reserved.
//

import UIKit
import CoreMotion

class ViewController: UIViewController {
    // Most conservative guess. We'll set them later.
    var maxX : CGFloat = 320;
    var maxY : CGFloat = 320;
    let boxSize : CGFloat = 30.0
    var boxes : Array<UIView> = []

    // For getting device motion updates
    let motionQueue = NSOperationQueue()
    let motionManager = CMMotionManager()

    override func viewDidLoad() {
        super.viewDidLoad()
        maxX = super.view.bounds.size.width - boxSize;
        maxY = super.view.bounds.size.height - boxSize;
        // Do any additional setup after loading the view, typically from a nib.
        createAnimatorStuff()
        generateBoxes()
    }

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        NSLog("Starting gravity")
        motionManager.startDeviceMotionUpdatesToQueue(motionQueue, withHandler: gravityUpdated)
    }

    override func viewDidDisappear(animated: Bool)  {
        super.viewDidDisappear(animated)
        NSLog("Stopping gravity")
        motionManager.stopDeviceMotionUpdates()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        NSLog("Memory warning")
        // Dispose of any resources that can be recreated.
    }

    func randomColor() -> UIColor {
        let red = CGFloat(CGFloat(arc4random()%100000)/100000)
        let green = CGFloat(CGFloat(arc4random()%100000)/100000)
        let blue = CGFloat(CGFloat(arc4random()%100000)/100000)

        return UIColor(red: red, green: green, blue: blue, alpha: 0.85);
    }

    func doesNotCollide(testRect: CGRect) -> Bool {
        for box : UIView in boxes {
            var viewRect = box.frame;
            if(CGRectIntersectsRect(testRect, viewRect)) {
                return false
            }
        }
        return true
    }

    func randomFrame() -> CGRect {
        var guess = CGRectMake(9, 9, 9, 9)

        do {
            let guessX = CGFloat(arc4random()) % maxX
            let guessY = CGFloat(arc4random()) % maxY;
            guess = CGRectMake(guessX, guessY, boxSize, boxSize);
        } while(!doesNotCollide(guess))

        return guess
    }

    func addBox(location: CGRect, color: UIColor) -> UIView {
        let newBox = UIButton(frame: location)
        newBox.backgroundColor = color

        view.addSubview(newBox)
        addBoxToBehaviors(newBox)
        self.boxes.append(newBox)
        newBox.tag = Int(arc4random())
        newBox.addTarget(self, action:"tapped:", forControlEvents: .TouchUpInside)
        return newBox
    }

    func tapped(sender: UIButton) {
        println("sender.tag: ", Int(sender.tag))
    }

    func generateBoxes() {
        for i in 0..<2 {
            var frame = randomFrame()
            var color = randomColor()
            var newBox = addBox(frame, color: color);
        }
    }

    var animator:UIDynamicAnimator? = nil;
    let gravity = UIGravityBehavior()
    let collider = UICollisionBehavior()
    let itemBehavior = UIDynamicItemBehavior()

    func createAnimatorStuff() {
        animator = UIDynamicAnimator(referenceView:self.view);

        gravity.gravityDirection = CGVectorMake(0, 0.8)
        animator?.addBehavior(gravity)

        // We're bouncin' off the walls
        collider.translatesReferenceBoundsIntoBoundary = true
        animator?.addBehavior(collider)

        itemBehavior.friction = 0.7
        itemBehavior.elasticity = 0.1
        animator?.addBehavior(itemBehavior)
    }

    func addBoxToBehaviors(box: UIView) {
        gravity.addItem(box)
        collider.addItem(box)
        itemBehavior.addItem(box)
    }

    //----------------- Core Motion
    func gravityUpdated(motion: CMDeviceMotion!, error: NSError!) {
        let grav : CMAcceleration = motion.gravity;

        let x = CGFloat(grav.x);
        let y = CGFloat(grav.y);
        var p = CGPointMake(x,y)

        if (error != nil) {
            NSLog("\(error)")
        }

        // Have to correct for orientation.
        var orientation = UIApplication.sharedApplication().statusBarOrientation;

        if(orientation == UIInterfaceOrientation.LandscapeLeft) {
            var t = p.x
            p.x = 0 - p.y
            p.y = t
        } else if (orientation == UIInterfaceOrientation.LandscapeRight) {
            var t = p.x
            p.x = p.y
            p.y = 0 - t
        } else if (orientation == UIInterfaceOrientation.PortraitUpsideDown) {
            p.x *= -1
            p.y *= -1
        }

        var v = CGVectorMake(p.x, 0 - p.y);
        gravity.gravityDirection = v;
    }
}

EDIT2

I just noticed that they don't have to touch each other to stop responding to UIGravityBehavior.

EDIT3

Ok, seems to be a bug in Swift somehow. I implemented the same in ObjC and there is no problem whatsoever.

1

There are 1 answers

0
LGP On

The problem is that the gravity update will cause a call to gravityUpdated() on a thread other than the main thread. You should keep your UI updates to the main thread.

To fix it, either switch to the main thread in your gravityUpdated(), like so:

func gravityUpdated(motion: CMDeviceMotion!, error: NSError!) {
    dispatch_async(dispatch_get_main_queue()) {
        // rest of your code here
    }
}

Or execute CMMotionManager on the main queue instead, like so:

This line

let motionQueue = NSOperationQueue()

changes to

let motionQueue = NSOperationQueue.mainQueue()