RickiG RickiG - 4 months ago 23
iOS Question

UICollisionBehavior - views pass through boundaries

As part of implementing a UICollisionBehaviour I set up boundaries for the screen edge.
I then added some views and finally attached a UIPanGestureRecognizer to one of them.

Now I can push around the smaller views with my draggable view.

Hammer-time!

Problem:
If I corner a smaller view and keep pushing it against the screen edge, it will eventually slip past the boundary and get trapped on the other side. My "hammer view" that I use to hit and push the other views around with will also get caught in the boundaries. I.e. it get's stuck/un-draggable against the side of the screen.

I did a very small example to see if I could reproduce it when I had very few views and no conflicting behaviours, views still go through the boundaries. Either UIDynamics can't handle very much, or (more likely) I am somehow configuring it wrong.

The small example below has the weird behaviour:

class ViewController: UIViewController {

var animator: UIDynamicAnimator?
var collisionBehaviour: UICollisionBehavior?
var panBehaviour: UIAttachmentBehavior?

override func viewDidLoad() {
super.viewDidLoad()
}

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
setup()
}

func setup() {
//setup collisionBehaviour and animator
collisionBehaviour = UICollisionBehavior()
collisionBehaviour!.collisionMode = UICollisionBehaviorMode.allZeros
animator = UIDynamicAnimator(referenceView: view)

//add boundaries
collisionBehaviour!.addBoundaryWithIdentifier("verticalMin", fromPoint: CGPointMake(0, 0), toPoint: CGPointMake(0, CGRectGetHeight(view.frame)))
collisionBehaviour!.addBoundaryWithIdentifier("verticalMax", fromPoint: CGPointMake(CGRectGetMaxX(view.frame), 0), toPoint: CGPointMake(CGRectGetMaxX(view.frame), CGRectGetHeight(view.frame)))
collisionBehaviour!.addBoundaryWithIdentifier("horizontalMin", fromPoint: CGPointMake(0, CGRectGetMaxY(view.frame)), toPoint: CGPointMake(CGRectGetMaxX(view.frame), CGRectGetMaxY(view.frame)))
collisionBehaviour!.addBoundaryWithIdentifier("horizontalMax", fromPoint: CGPointMake(0, 0), toPoint: CGPointMake(CGRectGetMaxX(view.frame), 0))

// collisionBehaviour!.translatesReferenceBoundsIntoBoundary = true // same effect as the above boundaries
//setup up some round views to push around
for i in 0..<5 {
let ball = UIView(frame: CGRectMake(0, 30, 50, 50))
ball.center = view.center
ball.backgroundColor = UIColor.greenColor()
ball.layer.cornerRadius = CGRectGetWidth(ball.frame) * 0.5
view.addSubview(ball)
collisionBehaviour!.addItem(ball)
}

//setup a hammer view which can be dragged and used to squeze the ball views of the screen
let hammer = UIView(frame: CGRectMake(0, 0, 100, 100))
hammer.backgroundColor = UIColor.redColor()
view.addSubview(hammer)
collisionBehaviour!.addItem(hammer)

let noRotationBehaviour = UIDynamicItemBehavior(items: [hammer])
noRotationBehaviour.allowsRotation = false
animator!.addBehavior(noRotationBehaviour)

let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("handlePan:"))
hammer.addGestureRecognizer(panGestureRecognizer)

//"start" the collision detection
animator!.addBehavior(collisionBehaviour!)
}

//Move the hammer around
func handlePan(recognizer: UIPanGestureRecognizer) {
if let view = recognizer.view {
let location = recognizer.locationInView(self.view)
switch recognizer.state {
case .Began:
panBehaviour = UIAttachmentBehavior(item: view, attachedToAnchor: location)
animator!.addBehavior(panBehaviour!)
println("begin")
case .Changed:
panBehaviour!.anchorPoint = location
println("change \(location)")
case .Ended:
println("ended")
animator!.removeBehavior(panBehaviour!)
default:
println("done")
}
}
}

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


}

Thanks for any help given.

Answer

Good news: you aren't doing anything wrong. Bad news: this is expected behavior. Your hammer is pushing the view against the reference boundaries until it finally breaks through because the physics of your model says that's what it should do. The reference boundary is not a boundary that can't be crossed, it only defines an area where your physics rules apply consistently.

This isn't really documented, but the page for UICollisionBehavior states:

When setting the initial position for a dynamic item, you must ensure that its bounds do not intersect any collision boundaries. The animation behavior for such a misplaced item is undefined.

This is only partially true, in your case (as with mine) if an item goes outside the boundary after initialization, it's behavior is also undefined.

I tried to arrange a set of balls on the right side of a view. There is an anchor point under the rightmost ball. The yellow view is the reference view and everything starts off fine... this works

But, as I added more balls to they point they could no longer fit, they would start to pop out. In fact, the one on the top right of the image popped out of the bottom center and rolled around the reference view counter clockwise to be near the anchor point. this is broken

UPDATE: For a solution you can set the collisionDelegate of your collisionBehaviors and capture the start and ends of the collisions and then move your view back into the boundary and away from your hammer to make it look like they escaped.

As you have figured out translatesReferenceBoundsIntoBoundary is equivalent to the set of addBoundaryWithIdentifier calls.

Use:

collisionBehaviour!.translatesReferenceBoundsIntoBoundary = true 

same as:

//add boundaries
    collisionBehaviour!.addBoundaryWithIdentifier("verticalMin", fromPoint: CGPointMake(0, 0), toPoint: CGPointMake(0, CGRectGetHeight(view.frame)))
    collisionBehaviour!.addBoundaryWithIdentifier("verticalMax", fromPoint: CGPointMake(CGRectGetMaxX(view.frame), 0), toPoint: CGPointMake(CGRectGetMaxX(view.frame), CGRectGetHeight(view.frame)))
    collisionBehaviour!.addBoundaryWithIdentifier("horizontalMin", fromPoint: CGPointMake(0, CGRectGetMaxY(view.frame)), toPoint: CGPointMake(CGRectGetMaxX(view.frame), CGRectGetMaxY(view.frame)))
    collisionBehaviour!.addBoundaryWithIdentifier("horizontalMax", fromPoint: CGPointMake(0, 0), toPoint: CGPointMake(CGRectGetMaxX(view.frame), 0))