Building a Dynamic Alert View
I stumbled across developer Victor Baro’s dynamic iOS “jelly view” (http://victorbaro.com/2014/07/vbfjellyview-tutorial/), which instantly caught my eye. This clever hack uses dynamic attachment behaviors that wiggle in harmony, enabling you to create views that emulate Jell-O. Although its utility is limited in practical deployment, it provides a superb example of how traditional iOS elements like alerts can be re-imagined using modern APIs. Figure 6-4 shows a jelly view alert in motion, squashing and stretching as it bounces off an invisible center ledge within the main UI.
Figure 6-4 This “jelly view” distorts its shape as it uses UIKit dynamics to emulate a view built onto a blob of Jell-O.
Connecting Up the Jelly
The secret to the jelly effect lies in an underlying 3×3 grid of tiny views, all attached to each other and to the main view’s center using UIAttachmentBehavior instances (see Figure 6-5). These views and their attachments create a semi-rigid backbone that provides the view physics. Listing 6-8 details how these views and attachments are made and installed. The elasticity of the connections allows the views to move toward and away from each other, creating a deformed skeleton for the view presentation.
Figure 6-5 The nine connected points form a spring-based skeleton for the Jell-O animation.
Listing 6-8 Establishing Jelly Dynamics
- (void) establishDynamics : (UIDynamicAnimator *) animator { if (animator) _animator = animator; // Create baseline dynamics for primary view UIDynamicItemBehavior *dynamic = [[UIDynamicItemBehavior alloc] initWithItems:@[self]]; dynamic.allowsRotation = NO; dynamic.elasticity = _elasticity / 2; dynamic.density = _density; dynamic.resistance = 0.9; [_animator addBehavior:dynamic]; // Establish jelly grid for (int i = 0; i < 9; i++) { // Add dynamics UIView *view = [self viewWithTag:(i + 1)]; UIDynamicItemBehavior *behavior = [[UIDynamicItemBehavior alloc] initWithItems:@[view]]; behavior.elasticity = _elasticity * 2; behavior.density = _density; behavior.resistance = 0.2; [_animator addBehavior:behavior]; // Attach each grid view to main jelly view center UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:view attachedToItem:self]; attachment.damping = _damping; attachment.frequency = _frequency; [_animator addBehavior:attachment]; // Attach views to each other if ((i + 1) != 5) // skip center { NSInteger xTag = [@[@(1), @(2), @(5), @(0), @(4), @(8), @(3), @(6), @(7)][i] integerValue] + 1; UIView *nextView = [self viewWithTag:xTag]; attachment = [[UIAttachmentBehavior alloc] initWithItem:view attachedToItem:nextView]; attachment.damping = _damping; attachment.frequency = _frequency; [_animator addBehavior:attachment]; } } }
Drawing the View
UIView instances are rectangular, not gelatinous. To create a view that looks as if it deforms, even if the underlying view remains rectangular, you must hide each of the underlying views from Figure 6-5 and draw a unified shape that represents the adjusted skeleton. You do this by observing changes on each of the component views. When they move, which you detect by observing the center property, the jelly view needs a redraw. Listing 6-9 shows the redrawing code.
This code works by building a Bezier path from corner point to corner point to corner point. It uses the center views along each edge as control points to produce its inflected curves. Once the curved path is calculated, a standard drawRect: method fills in the curve to present the view.
Listing 6-9 Drawing the Jelly View
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // Update whenever a child view center changes [self setNeedsDisplay]; } - (UIBezierPath *) cornerCurve { // Build a series of quad curve elements from point to point to point UIBezierPath *path = [UIBezierPath bezierPath]; UIView *v0 = [self viewWithTag:1]; [path moveToPoint:v0.center]; // The corner points are view destinations. // The centers act as control points. NSArray *destinations = @[@(2), @(8), @(6), @(0)]; NSArray *controlPoints = @[@(1), @(5), @(7), @(3)]; for (int i = 0; i < 4; i++) { NSInteger dTag = [destinations[i] integerValue] + 1; NSInteger cTag = [controlPoints[i] integerValue] + 1; UIView *vd = [self viewWithTag:dTag]; UIView *vc = [self viewWithTag:cTag]; [path addQuadCurveToPoint:vd.center controlPoint:vc.center]; } return path; } - (void) drawRect:(CGRect)rect { // Build the curves and draw the shape [_color set]; [[self cornerCurve] fill];}
Deploying Jelly
While the jelly view is fun to create, deploy with care. Most users have a fixed limit of patience. Any dynamic elements will tend to run longer in presentation and dismissal than standard system-supplied UI elements. They have more complicated visual stories to tell. Because of this, you might need to trade off the cool visual flourishes that excite a developer if you want to put the user experience first. A jelly-based alert may be exciting to develop, but an overly long alert that takes precious seconds to settle may add one-star reviews to your product.
A user will not be able to tell if your app was developed using UIKit, OpenGL, Cocos2D, or SpriteKit. Just because you can now do exciting dynamics in UIKit is not sufficient reason to include those solutions. Your apps must defer to and serve the needs of your users rather than pad your resume and augment your portfolio. Keep this in mind and use dynamic animators sparingly.