Clicky

iOS Dev Nugget 79 TweetBot Photo Flicking Effect

.

Need to run a code review on your codebase? Hire me

With UIKit Dynamics, it's easier than ever to make springy user interface such as the one shown when flicking to dismiss a photo in the popular TweetBot app from Tapbots.

Just set up the animator and a UISnapBehavior which snaps the photo (in the example code below, the _cardView) back into place when flicked.

Most of the code lies in the UIPanGestureRecognizer's delegate method.

//ivars, somewhere
/*
CGPoint _centerWhenStartDragging;
UIAttachmentBehavior* _dragBehavior;
UIDynamicAnimator* _animator;
UISnapBehavior* _snapBehavior;
UIView* _cardView;
*/

- (void)setUp {
    UIPanGestureRecognizer* panGestureRecognizer =
        [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragged:)];
    panGestureRecognizer.minimumNumberOfTouches = 1;
    panGestureRecognizer.maximumNumberOfTouches = 1;
    [self addGestureRecognizer:panGestureRecognizer];

    _animator = [[UIDynamicAnimator alloc] initWithReferenceView:self];
    _snapBehavior = [[UISnapBehavior alloc] initWithItem:_cardView snapToPoint:self.center];
    _snapBehavior.damping = 0.7;
    [@animator addBehavior:_snapBehavior];
}

- (void)dragged:(UIPanGestureRecognizer*)aGestureRecognizer {
    CGPoint ptInCard = [aGestureRecognizer locationInView:_cardView];
    CGPoint ptInView = [aGestureRecognizer locationInView:self];

    switch (aGestureRecognizer.state) {
        case UIGestureRecognizerStateBegan:
            _centerWhenStartDragging = _cardView.center;
            UIOffset offset = UIOffsetMake(ptInCard.x -
                _cardView.frame.size.width/2, ptInCard.y - _cardView.frame.size.height/2);
            //The anchor and the offset refers to the same point, where the finger is
            _dragBehavior = [[UIAttachmentBehavior alloc] initWithItem:_cardView
                offsetFromCenter:offset attachedToAnchor:ptInView];
            [_animator addBehavior:_dragBehavior];
            break;
        case UIGestureRecognizerStateChanged:
            //Keep the anchor as where the finger is
            _dragBehavior.anchorPoint = ptInView;
            break;
        case UIGestureRecognizerStateEnded:
            [_animator removeBehavior:_dragBehavior];
            _dragBehavior = nil;
            CGGloat diff = (_cardView.center.x - _centerWhenStartDragging.x)*(_cardView.center.x - _centerWhenStartDragging.x) +
                (_cardView.center.y - _centerWhenStartDragging.y)*(_cardView.center.y - _centerWhenStartDragging.y);
            CGPoint velocity = [aGestureRecognizer velocityInView:self];
            //Magic number, can be tweaked
            if (diff > 60*60 && [self flickTowardsCornersOfScreen:velocity pt:ptInView]) {
                _animator.removeBehavior(_snapBehavior);
                _snapBehavior = nil;
                UIDynamicItemBehavior* dynamic =
                    [[UIDynamicItemBehavior alloc] initWithItems:@[_cardView]];
                [dynamic addLinearVelocity:velocity/10 forItem:_cardView];
                dynamic.action = ^{
                    if (!CGRectIntersectsRect(bounds, _cardView.frame)) {
                        [self dismiss];
                    }
                };
                [_animator addBehavior:dynamic];
                //add a little gravity so it accelerates off the screen
                //(in case user gesture was slow)
                UIGravityBehavior* gravity =
                    [[UIGravityBehavior alloc] initWithItems:@[_cardView]];
                gravity.magnitude = 0.2;
                gravity.gravityDirection = CGVectorMake(velocity.x, velocity.y);
                [_animator addBehavior:gravity];
            } else {
                NSLog("snap automatically");
            }
            break;
        default:
            break;
    }
}

Briefly:

When UIGestureRecognizerStateBegan, you remember where the user tapped to start dragging/flicking, and set up a UIAttachmentBehavior that follows the user's finger.

When UIGestureRecognizerStateChanged, you update the drag behavior to follow the user's finger.

When UIGestureRecognizerStateEnded, you dismiss cardView if the user has flicked fast enough towards the corners of the screen, adding a gravity to it so cardView moves fast enough. If the user haven't flicked hard enough or towards the edge of the screen, just allow cardView to snap back to its original position.

Jared Sinclair wrote and open sourced view controller that does this in a similar manner called JTSImageViewController. Check it out.


Your feedback is valuable: Do you want more nuggets like this?   Yes   or   No

.

.

Like this and want such iOS dev nuggets to be emailed to you, weekly?

Sign Me Up! or follow @iosdevnuggets on Twitter

.

View archives of past issues