Since iOS 6 the Mail app has a Pull to Refresh
indicator:
In this article I’ll show how to implement such an animation from scratch. This is for learning and practice purposes only, there is no need to implement this yourself. You can enable the Pull to Refresh
behavior for any UITableViewController
by setting a UIRefreshControl
:
@implementationMyTableViewController{-(void)viewDidLoad{[superviewDidLoad];UIRefreshControl*refreshControl=[[UIRefreshControlalloc]init];[refreshControladdTarget:selfaction:@selector(refresh)forControlEvents:UIControlEventValueChanged];self.refreshControl=refreshControl;}-(void)refresh{NSLog(@"Refresh!");[self.refreshControlendRefreshing];}@end
For older iOS versions or UIViewControllers
classes ODRefreshControl
written by Fabio Ritrovato does the job.
Drawing the reload icon
First step is to draw the reload arrow in the icon. This is resized during the animation, so it needs to be drawn as path:
As starting point I drew the shape in Adobe Illustrator by deleting an arc from a circle, converting the path to a shape using Object > Path > Outline Stroke
and merging it with a triangle using Pathfinder > Unite
:
Now, how to take this to an iOS app?
Drawing a shape in an iOS app
The easiest way to draw an arbitrary shape is to subclass UIView
, override drawRect
and draw a path using UIBezierPath
. Something like this:
#import "ShapeView.h"@implementationShapeView{UIBezierPath*_arc,*_arrow;}-(id)initWithFrame:(CGRect)frame{self=[superinitWithFrame:frame];if(self){CGPointcenter=CGPointMake(100,100);floatr=34;_arc=[UIBezierPathbezierPath];[_arcaddArcWithCenter:centerradius:rstartAngle:0endAngle:M_PI*1.5fclockwise:YES];_arc.lineWidth=14;_arrow=[UIBezierPathbezierPath];CGPointp=CGPointMake(center.x,center.y-r);[_arrowmoveToPoint:CGPointMake(p.x,p.y-20)];[_arrowaddLineToPoint:CGPointMake(p.x+32,p.y)];[_arrowaddLineToPoint:CGPointMake(p.x,p.y+20)];self.backgroundColor=UIColor.whiteColor;}returnself;}-(void)drawRect:(CGRect)rect{[[UIColorredColor]setStroke];[[UIColorredColor]setFill];[_arcstroke];[_arrowfill];}@end
Certainly possible, but not good. Manually writing drawing code isn’t fun even for such a simple shape. There has to be a better way! How about using a SVG graphic?
The path is encoded in the SVG in a compact format, so it should be possible to use this:
<pathfill="#ED1F24"d="M76.104,56.276c0,14.517-11.812,26.327-26.328,26.327c-14.518,0-26.329-11.811-26.329-26.327 c0-14.5,11.783-26.3,26.278-26.327V46.36l31.259-19.965L49.725,3.937v12.615c-21.882,0.027-39.675,17.837-39.675,39.725 C10.049,78.181,27.87,96,49.775,96C71.68,96,89.5,78.181,89.5,56.276H76.104z"/>
Creating a UIBezierPath from a SVG
Arthur Evstifeev wrote UIBezierPath-SVG
: a parser to create a UIBezierPath
from a SVG path. It doesn’t support ARC, so it is required to disable ARC for this single file. Otherwise it works perfectly and is a very nice category for UIBezierPath
:
#import "ShapeView.h"#import "UIBezierPath+SVG.h"@implementationShapeView{UIBezierPath*_arrow;}-(id)initWithFrame:(CGRect)frame{self=[superinitWithFrame:frame];if(self){_arrow=[UIBezierPathbezierPathWithSVGString:@"M76.104,56.276c0,14.517-11.812,26.327-26.328,26.327c-14.518,0-26.329-11.811-26.329-26.327,c0-14.5,11.783-26.3,26.278-26.327V46.36l31.259-19.965L49.725,3.937v12.615c-21.882,0.027-39.675,17.837-39.675,39.725,C10.049,78.181,27.87,96,49.775,96C71.68,96,89.5,78.181,89.5,56.276H76.104z"];self.backgroundColor=[UIColorwhiteColor];}returnself;}-(void)drawRect:(CGRect)rect{[[UIColorredColor]setFill];[_arrowfill];}@end
Out of curiosity, I wondered if I could capture a video of the animation from the hardware device, as the simulator doesn’t feature the Mail app:
Recording the iPhone screen
Reflector app does the trick via AirPlay Mirroring, GIF Brewery converts a movie to an animated GIF:
Creating the drop shape
As starting point for the drop shape, I traced the shape in illustrator as well and placed it in a way so that the coordinates are convenient for later positioning.
For the colors, UIColorFromRGB.h
(or alternatively Panic's Developer Color Picker
) comes in handy to use hex color values in Objective C code:
Applying the same old bezierPathWithSVGString
trick:
#import "ShapeView.h"#import "UIBezierPath+SVG.h"#import "UIColorFromRGB.h"@implementationShapeView{UIBezierPath*_arrow,*_drop;}-(id)initWithFrame:(CGRect)frame{self=[superinitWithFrame:frame];if(self){_arrow=[UIBezierPathbezierPathWithSVGString:@"M9.906,29.144c0,5.371-4.37,9.741-9.741,9.741c-5.372,0-9.742-4.37-9.742-9.741 c0-5.365,4.359-9.73,9.724-9.741v6.072l11.565-7.387L0.146,9.778v4.667c-8.097,0.01-14.681,6.6-14.681,14.698 c0,8.104,6.595,14.697,14.699,14.697c8.104,0,14.698-6.593,14.698-14.698H9.906z"];_drop=[UIBezierPathbezierPathWithSVGString:@"M0,0c-14.359,0-26,11.641-26,26c0,4.729,1.126,8.741,3,13c11,25,12,88,12,88 c0,6.076,4.924,11,11,11s11-4.924,11-11c0,0,1-63,12-88c1.874-4.259,3-8.271,3-13C26,11.641,14.359,0,0,0z"];self.backgroundColor=UIColorFromRGB(0xe2e7ed);}returnself;}-(void)drawRect:(CGRect)rect{CGContextRefctx=UIGraphicsGetCurrentContext();CGContextTranslateCTM(ctx,rect.size.width/2.f,10);[UIColorFromRGB(0x9ba2ab)setFill];[_dropfill];[[UIColorwhiteColor]setFill];[_arrowfill];}@end
Creating a unit test to validate the drawing code using reference images
With Gabriel Handford’sGHUnit being my favorite test framework for iOS, I decided to give its UI Testing Component written by John Boiles a try. After the setup procedure for GHUnit, views can be verified pixel-by-pixel in one single line of code by subclassing GHViewTestCase and calling GHVerifyView
:
#import <GHUnitIOS/GHUnit.h>#import "ShapeView.h"@interfaceShapeViewTest : GHViewTestCase{}@end@implementationShapeViewTest-(void)testShapeView{GHVerifyView([[ShapeViewalloc]initWithFrame:CGRectMake(0,0,640,200)]);}@end
Especially the feature to approve changes with a single click comes in quite handy:
The reference images are stored in the Simulator Documents folder. There are scripts to copy the reference images from there to the project folder and vice versa. CopyTestImages.sh
is supposed to be in the root project folder and copies the images into a TestImages
project folder:
PrepareUITests.sh
is supposed to be run from the Tests build phase to set up the images before running the actual tests:
So I used the Xcode Organizer’s Screenshot feature to capture some still frames at different view heights, cropped them using ImageMagick like this:
convert -crop 640x127+0+128 Screenshot.png ShapeViewTest-testShape-1-0-ShapeView.png
and copied them to the TestImages
folder. Voila:
To be continued. Next time:
- converting the SVG path to code that can be animated
- find the right sizes for the arcs depending on the view height
- parameterizing the shape drawing