iOS 2D Drawing App using Quartz 2D August 9, 2011
Posted by henrychin in ipad app.Tags: draw, graphics, iOS, iPhone, mobile, objective-c, Quartz 2D
add a comment
iOS has a colossal list of frameworks and APIs to help developers create amazing apps. In particular Quartz 2D provides the basis for any drawing application. Some of the features offered by Quartz 2D include path-based drawing, painting with transparency, shading and drawing shadows just to name of few. Check apple documentation in XCode Organizer or on Apple developer website (http://developer.apple.com/library/ios/navigation/) and search for Quartz 2D. I was curious about how 2D drawing works using Quartz 2D and so I decided to use developers favourite tool, “Google” to find sample apps that demonstrate drawing basic shapes such as rectangle, ellipse and lines using iOS device (iPhone / iPad). If you ever tried OmniGraffle on iPad then a much simpler version. Google’s quick response showed an endlist selection of websites but nothing interesting or worthwhile. So I decided to embark on creating a demo app that can draw basic shapes. I am going to dispense with pleasantry and assume that you have basic knowledge of Objective-C and C programming language. You should be pretty comfortable with XCode, I am using the latest XCode 4 and iPhone v4.1 and iPad v4.3 release.
Step 1
Create a new XCode project by selecting File->New->New Project. Which should bring up template window that shows various templates to choose from. Select View Based Application and press next button. Next enter product name, company identifier and select iPad for Device Family.
By default SampleDrawApp folder contains three sets of files. The first set is SampleDrawAppDelegate files that include header (.h) and source (.m) files. Next is the controller files (SampleDrawAppViewController) including nib file. Finally is the MainWindow.xib file to round out the files in this folder.
Step 2
Create canvas class to allow 2D draw objects to be drawn on. This class will serve as the main UIView that will allow sub-views to add/remove draw objects later on. In addition, this class will be used to link ViewController with custom UIView class (i.e. Canvas). To create a new class first select SampleDrawApp folder in the left-hand pane. From main menu select File -> New -> New File menu item. Dialog box will appear, select Objective-C Class icon. Two files will be automatically created, Canvas.h and Canvas.m. Modify Canvas.h to include three new properties called origin, width and height. Make sure to synthesize the properties in corresponding class file. Here is what it should look like. Canvas.h
#import @interface Canvas : UIView {
CGPoint origin; CGFloat width; CGFloat height;
}
@property (nonatomic, assign) CGPoint origin;
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
@end
Canvas.m
#import "Canvas.h"
@implementation Canvas
@synthesize origin;
@synthesize width;
@synthesize height;
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
return self;
}
/* // Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect { // Drawing code
} */
- (void)dealloc { [super dealloc]; } @end
Next link SampleDrawAppViewController.xib file to Canvas custom class by selecting the xib file to bring up Interface Builder. Select Canvas view and bring up Identify Inspector pane. Under Custom Class select Canvas and save the file. Now comes the interesting part where we make changes to Canvas.m to support drawing 2D objects.
Step 3
Add initWithCoder method instead of initWithFrame method when initializing a UIView class from xib file. Normally, you would overwrite initWithFrame, but this is a special case where xib file is creating the UIView. Set default properties for origin, width and height to look something like this.
- (id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
NSLog(@"initWithCoder");
self.origin = CGPointMake(0,0);
self.width = 0; self.height = 0;
}
return self;
}
Step 4
drawRect() method is the main callback that enables the app to draw custom drawing on user interface. In order to draw the app uses Core Graphics API to perform a lot of the drawing. To start off, the CGContextRef is used to provide a context for drawing. Therefore any drawing must pass along CGContextRef object. Before we can draw anything, we need to get acces to the context to begin with. By default Core Graphics will instantiate an instance of this object before calling drawRect method. This allows the code to call UIGraphicsGetCurrentContext() to retrieve the context. Note, calling this method from within another method (other than drawRect) will cause UIGraphicsGetCurrentContext() to return nil instance. To see a line on the screen you need to set the line width by calling CGContextSetLineWidth(context, 2.0). This creates a line of width 2 pixels wide. The default color for any line is black so setting the color will be helpful visually. Use the command CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);to change to color to blue. Next create CGRect object to store origin, width and height information. Here is what it should look like:
- (void)drawRect:(CGRect)rect {
NSLog(@"drawRec"); // Get current context
CGContextRef context = UIGraphicsGetCurrentContext();
// Set line width
CGContextSetLineWidth(context, 2.0);
// Set color
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
// Create rectangle co-ordinates to draw
CGRect rectangle = CGRectMake(self.origin.x,self.origin.y,self.width,self.height);
if (self.width != 0 && self.height != 0) {
[Canvas drawRectangle:context useDimension:rectangle];
}
}
Ignore the method call for drawRectangle for now, I will talk about this in the next step.
Step 5
drawRectangle method takes CGRect information and draws a simple rectangle using Core Graphics. To draw a rectangle use CGContextAddRect method. To add some visual appeal fill the rectangle with red color like this:
+ (void) drawRectangle:(CGContextRef)context useDimension:(CGRect)rectangle {
NSLog(@"drawRectangle");
CGContextAddRect(context, rectangle);
CGContextStrokePath(context);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor); CGContextFillRect(context, rectangle);
}
Notice that this method is define as a class method so make sure to add method definition in the interface file.
Step 6
Next step involves implementing UIResponder touch methods to process when a user touch the screen and how user interface should respond. Here is the code for now, I will explain the content in more details later.
// touch gesture has begun, collect touch point
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[event allTouches] anyObject];
self.origin = [touch locationInView:touch.view];
self.width = 0;
self.height = 0;
NSLog(@"xOrigin=%f, yOrigin=%f", self.origin.x, self.origin.y);
}
// touch gesture movement detected, set width and height measurements
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { // get current X,Y co-ordinates
UITouch *touch = [[event allTouches] anyObject];
CGPoint offset = [touch locationInView:touch.view];
self.width = offset.x - self.origin.x;
self.height = offset.y - self.origin.y;
// update display to dynamically show object being drawn
[self setNeedsDisplay];
}
// touch gesture ended, finalize any drawings details
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { // store co-ordinates in drawObject for display for drawRect method
if (self.width != 0 && self.height != 0) {
// add object to canvas for display
self.width = 0;
self.height = 0;
// update display
[self setNeedsDisplay];
}
}
// touch gesture canceled
- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
// NSLog(@">>>touchesCancelled");
}
Step 7
Run the app and see what it does. When you run the app tap and drag and you will see a red rectangle being drawn. When you let go notice how the image disappears. Well this is because the image is not being stored as a UIView subview to the Canvas. More on this later.
