Detecting When UIGravityBehavior is “Off Screen"

A Simple Implementation

Yesterday I was working on some UI enhancements to a new feature that is coming for RabbleTV. One of the pieces to the UI envolved using UIDynamics…specifically UIGravityBehavior. This was going to be a pretty straightforward implementation considering I didn’t need to use the UIGravityBehavior with any other types as I had done with previous apps.

Assumptions Are Bad

During some testing I noticed that the CPU would spike during the animation, but never go back down after I assumed the animation was complete…in my case the “fall” passed the referced view’s bounds. I didn’t think too much of it at the time because I still needed to add in my completionHandler. I kicked the can down the road for a few hours until I could profile it. I assumed it must have been a coincidence since I’m also making network calls during this animation as well.

Upon the first run of my now completed UI animation the completionHandler wasn’t called. I checked and doubled checked my code and all the appropriate delegates and properties were set. The next part of my debugging strategy was to see when exactly the behavior was stopped. Perhaps I was trying to perform an action before everything had been completed. This is where my assumption bit me.

I had assumed that UIGravityBehavior was completing, but in reality it wasn’t. I was able to verify this by logging the current point in the reference view the item was at using linearVelocityForItem.

The fall was infinite. After I stopped and thought about it it made sense. If the UIGravityBehavior is supposed to represent gravity on an object and space is infinite then why would it ever stop. I had never run into this before because in all my other experiences of using UIDynamics I used UIGravityBehavior inconjunction with other behaviors.

Choose Your Solution

As I saw it I had two possible soultions to implement to fix my issue.

First

Use UICollisionBehavior. There really isn’t much more to say there. You can setTranslatesReferenceBoundsIntoBoundaryWithInsets to setup the area where you want the items to “stop”.

Second

Add a UIDynamicBehavior that checks for the Y coordinate as the items are falling (specifically the last item). Once it is past the height of the reference view then remove the behaviors.

And the winner is…

I opted for the second approach because it gave me more control over when to stop the animation. Once I updated my animation controller all of the delegate and completionHandlers were properly called.

Code Snippet

// MARK: Public

- (void)animateItemsWithCompletionBlock:(RTVStandardCompletionBlock)block {

    if (block) {
        self.animationCompletionBlock = [block copy];
    }

    self.animator.delegate = self;

    NSArray *referenceItems = self.itemReferences.allObjects;
    
    /**
     * Gravity Behavior
     */

    UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:referenceItems];

    [self.animator addBehavior:gravityBehavior];
    
    /**
     * Dynamic Behavior
     *
     * @note
     * I'm adding the dynamic behavior so that I can tell when the last item
     * has fallen past the bottom of the screen. Once it has then I remove all
     * the behaviors. This will trigger the animator delegate method, which will
     * call the completionBlock.
     *
     * Without doing this the view continues to "fall" and eats up the CPU.
     * Another possible solution is to setup a collision barrier which should
     * trigger it as well.
     */
    
    UIDynamicItemBehavior *dynamicItemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:referenceItems];
    
    __weak UIDynamicItemBehavior *weakBehavior = dynamicItemBehavior;
    __weak typeof(self) weakSelf = self;

    dynamicItemBehavior.action = ^{

        /**
         * @note
         * You only need to wait for the last object to finish (drop below) as 
         * opposed to iterating over all the item.
         */

        CGFloat currentY = [weakBehavior linearVelocityForItem:referenceItems.lastObject].y;
        
        if (currentY > CGRectGetMaxY(self.animator.referenceView.frame)) {
            [weakSelf.animator removeAllBehaviors];
        }
    };

    [self.animator addBehavior:dynamicItemBehavior];
    
    /**
     * Implict Animation of Alpha
     */
    
    [referenceItems enumerateObjectsUsingBlock:^(UIView *item, NSUInteger idx, BOOL *stop){

        [UIView animateWithDuration:0.65
                              delay:0
                            options:kNilOptions
                         animations:^{
                             item.alpha = 0.0f;
                         }
                         completion:nil];
    }];
}

Lessons (Re)Learned

  1. Space is inifinite
  2. Never assume anything