iosswiftanimationuikit-dynamicsuidynamicanimator

Swift: UIDynamic animate panel from bottom to top


I want to build a simple custom view which slides up from the bottom with UIDynamicAnimator.

When the app loads I offset the view from the screen using:

panelWidth   = originView!.frame.size.width
panelHeight  = originView!.frame.size.height/2
screenHeight = originView!.frame.size.height

// Set the frame for the container of this view based on the parent
container.frame = CGRectMake(0, screenHeight, panelWidth, panelHeight)

To envision this, I expect the view to appear as:

|-----|
|     |
|  A  | <--- Main View 
|     | 
|-----|
|-----|
|  B  | <--- Second View (offset by screen height so it draws directly under (0,height))
|-----|

I have set up gesture recognisers to detect when I swipe up and down (up to show, down to dismiss), however right now the view bounces off the screen and I can never seem to get it back.

With my animator, I use the following code

animator.removeAllBehaviors()
isOpen = open

let gravityY:CGFloat  = (open) ? -0.5 : 0.5 
let gravityX:CGFloat  = 0  
let magnitude:CGFloat = (open) ? -20 : 20
let boundaryY:CGFloat = (open) ? -panelHeight : panelHeight
let boundaryX:CGFloat = (open) ? 0 : 0

Before showing the rest of the code, I think I should discuss my constants. Vector math is not my strong point so this is probably the sole reason all of this is going wrong. I set the values to be negative on when open is true because I want to animate the view back onto the screen which means animating it upward, therefore I assume I need negative gravity.

I set boundaryY to be the height of the panel as I want to animate it up onto the screen only to the height of the window, currently it flies off the top (when I debugged screen height = 667 and panelHeight = 333.5 so I don't understand why it exceeds that mark)

// animator behaviours - here I just create animaters on the panels container
let gravityBehaviour:UIGravityBehavior = UIGravityBehavior(items: [container])
let collisionBehaviour:UICollisionBehavior = UICollisionBehavior(items: [container])
let pushBehaviour:UIPushBehavior = UIPushBehavior(items: [container], mode: UIPushBehaviorMode.Instantaneous)
let panelBehaviour:UIDynamicItemBehavior = UIDynamicItemBehavior(items:[container])

// Gravity behaviour - I don't want it to move in X direction, only Y
gravityBehaviour.gravityDirection = CGVectorMake(gravityX, gravityY)

// collision detection - I think this is the major problem, how does this work...
collisionBehaviour.addBoundaryWithIdentifier("basePanelBoundary", fromPoint: CGPointMake(boundaryX, 0), toPoint: CGPointMake(boundaryX, boundaryY))

// push behaviours
pushBehaviour.magnitude = magnitude

// panel behaviours
panelBehaviour.elasticity = 0.3

// bind behaviours
animator.addBehavior(gravityBehaviour)
animator.addBehavior(collisionBehaviour)
animator.addBehavior(pushBehaviour)
animator.addBehavior(panelBehaviour)

I'm mainly concerned with the collision detection as I believe this is what is causing the view to rotate and fling off the screen, how can I fix this?


Solution

  • After some further research I realised I was using far too many components to achieve the animation I desired, I also realised my boundaries were poorly defined and this is what was causing my panel to shoot off the window.

    To fix the issue I first amended my boundaries so that they would co-inside with the coordinates I specified on the screen:

    collisionBehavior.addBoundaryWithIdentifier("upperBoundary", fromPoint: CGPointMake(0, screenHeight - panelHeight), toPoint: CGPointMake(boundaryX, screenHeight - panelHeight))
    collisionBehavior.addBoundaryWithIdentifier("lowerBoundary", fromPoint: CGPointMake(0, screenHeight+panelHeight), toPoint: CGPointMake(boundaryX, screenHeight+panelHeight))
    

    I then defined a gravity vector thats value was influenced based on the value of the open boolean toggle:

    let gravityY:CGFloat  = (open) ? -1.5 : 1.5
    gravityBehavior.gravityDirection = CGVectorMake(0, gravityY)
    

    Finally I bound the behaviours to the animator and the animation worked as expected.

    In summary, if you are new to UIDynamics, I'd advise spending some time on paper before diving in and getting your hands dirty with the code, once I decomposed this problem it was far easier than my first attempt.

    Working Code

    func showBasePanel(open:Bool)
    {
        animator.removeAllBehaviors()
        isOpen = open
    
        // Define constants to be plugged into animator
        let gravityY:CGFloat  = (open) ? -1.5 : 1.5
        let boundaryY:CGFloat = panelHeight
        let boundaryX:CGFloat = panelWidth
    
        // animator behaviours
        let gravityBehavior:UIGravityBehavior = UIGravityBehavior(items: [container])
        let collisionBehavior:UICollisionBehavior = UICollisionBehavior(items:[container])
        let panelBehavior:UIDynamicItemBehavior = UIDynamicItemBehavior(items:[container])
    
        // Set the behvaiours
        panelBehavior.allowsRotation = false
        panelBehavior.elasticity = 0
    
        // Set collision behaviours
        collisionBehavior.addBoundaryWithIdentifier("upperBoundary", fromPoint: CGPointMake(0, screenHeight - panelHeight), toPoint: CGPointMake(boundaryX, screenHeight - panelHeight))
        collisionBehavior.addBoundaryWithIdentifier("lowerBoundary", fromPoint: CGPointMake(0, screenHeight+panelHeight), toPoint: CGPointMake(boundaryX, screenHeight+panelHeight))
        gravityBehavior.gravityDirection = CGVectorMake(0, gravityY)
    
        // set the animator behvaiours
        animator.addBehavior(gravityBehavior)
        animator.addBehavior(panelBehavior)
        animator.addBehavior(collisionBehavior)
    }