User interacting with gold iPhone

Effective Thinking

How to build a Simple Painting App for iOS

This article explains how to build a simple painting app for iOS. The aim is to demonstrate how a simple programming concept has to evolve to create a good user experience with high performance.

The Naive Approach to Painting in iOS

The starting approach to painting on iOS is to simply capture touch events and draw a line between the points. We can create and attach a custom UIView to the root view controller to handle all this functionality.

// in our root view controller
- (void)viewDidLoad {
    [super viewDidLoad];
    PaintView *paint = [[PaintView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:paint];
    [paint release];
}

// PaintView.m
@implementation PaintView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        hue = 0.0;
    }
    return self;
}

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    touch = [touches anyObject];
    [self setNeedsDisplay];
}

- (void)drawRect:(CGRect)rect {
    if(touch != nil){
        CGContextRef context = UIGraphicsGetCurrentContext();

        hue += 0.005;
        if(hue > 1.0) hue = 0.0;
        UIColor *color = [UIColor colorWithHue:hue saturation:0.7 brightness:1.0 alpha:1.0];

        CGContextSetStrokeColorWithColor(context, [color CGColor]);
        CGContextSetLineCap(context, kCGLineCapRound);
        CGContextSetLineWidth(context, 15);

        CGPoint lastPoint = [touch previousLocationInView:self];
        CGPoint newPoint = [touch locationInView:self];

        CGContextMoveToPoint(context, lastPoint.x, lastPoint.y);
        CGContextAddLineToPoint(context, newPoint.x, newPoint.y);
        CGContextStrokePath(context);
    }
}

@end

The code is very straightforward and appears to do exactly what we want. When a touch is captured, we save the touch object and ask the screen to update its display which will call (void)drawRect to draw a new segment of line. Unfortunately when you run the code you’ll see a big problem with this solution. The painting will flicker as you draw your finger along the screen.

The reason this happens is that the render engine is double buffered, so each time you issue a call to draw a line segment, it tick-tocks between the two buffers. Our naive attempt to draw to the screen needs a new solution to work properly.

Next Step: Adding in a Backing Store

The way to solve this problem is to draw to a backing store first, then when we want to update the screen we simply draw that backing store to the front buffer. To handle the backing store we need to create a new cgcontext tied to a bitmap.

@implementation PaintView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        hue = 0.0;
        [self initContext:frame.size];
    }
    return self;
}

- (BOOL) initContext:(CGSize)size {

	int bitmapByteCount;
	int	bitmapBytesPerRow;

	// Declare the number of bytes per row. Each pixel in the bitmap in this
	// example is represented by 4 bytes; 8 bits each of red, green, blue, and
	// alpha.
	bitmapBytesPerRow = (size.width * 4);
	bitmapByteCount = (bitmapBytesPerRow * size.height);

	// Allocate memory for image data. This is the destination in memory
	// where any drawing to the bitmap context will be rendered.
	cacheBitmap = malloc( bitmapByteCount );
	if (cacheBitmap == NULL){
		return NO;
	}
	cacheContext = CGBitmapContextCreate (cacheBitmap, size.width, size.height, 8, bitmapBytesPerRow, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaNoneSkipFirst);
	return YES;
}

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    [self drawToCache:touch];
}

- (void) drawToCache:(UITouch*)touch {
    hue += 0.005;
    if(hue > 1.0) hue = 0.0;
    UIColor *color = [UIColor colorWithHue:hue saturation:0.7 brightness:1.0 alpha:1.0];

    CGContextSetStrokeColorWithColor(cacheContext, [color CGColor]);
    CGContextSetLineCap(cacheContext, kCGLineCapRound);
    CGContextSetLineWidth(cacheContext, 15);

    CGPoint lastPoint = [touch previousLocationInView:self];
    CGPoint newPoint = [touch locationInView:self];

    CGContextMoveToPoint(cacheContext, lastPoint.x, lastPoint.y);
    CGContextAddLineToPoint(cacheContext, newPoint.x, newPoint.y);
    CGContextStrokePath(cacheContext);
    [self setNeedsDisplay];
}

- (void) drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGImageRef cacheImage = CGBitmapContextCreateImage(cacheContext);
    CGContextDrawImage(context, self.bounds, cacheImage);
    CGImageRelease(cacheImage);
}

@end

When the view is created we create a cached bitmap and cached context to handle all of our touch drawing, then in our drawRect callback we now grab a image snapshot of the cached context to draw to the front buffer. The full drawing is now shown every time you move your finger around the display.

Next Step: Optimizing the Drawing Code

At this point we have a working solution, but we don’t have a good solution. Depending on the hardware you run this on, you may notice that the drawing is having a hard time keeping up with your finger. The reason why is because we’re wasting a lot of time updating areas of the screen that aren’t changing, we’re simply rendering everything in the backing store 50 to 60 times per second. To solve this we want to tell the draw routine to only update the parts of the display that have changed. To do this we create a bounding box around the line being drawn and pass that box on to the drawRect routine.

- (void) drawToCache:(UITouch*)touch {
    hue += 0.005;
    if(hue > 1.0) hue = 0.0;
    UIColor *color = [UIColor colorWithHue:hue saturation:0.7 brightness:1.0 alpha:1.0];

    CGContextSetStrokeColorWithColor(cacheContext, [color CGColor]);
    CGContextSetLineCap(cacheContext, kCGLineCapRound);
    CGContextSetLineWidth(cacheContext, 15);

    CGPoint lastPoint = [touch previousLocationInView:self];
    CGPoint newPoint = [touch locationInView:self];

    CGContextMoveToPoint(cacheContext, lastPoint.x, lastPoint.y);
    CGContextAddLineToPoint(cacheContext, newPoint.x, newPoint.y);
    CGContextStrokePath(cacheContext);

    CGRect dirtyPoint1 = CGRectMake(lastPoint.x-10, lastPoint.y-10, 20, 20);
    CGRect dirtyPoint2 = CGRectMake(newPoint.x-10, newPoint.y-10, 20, 20);
    [self setNeedsDisplayInRect:CGRectUnion(dirtyPoint1, dirtyPoint2)];
}

We now have a bounding box around our touch points that includes a bit of padding to accommodate the line width. At this point we have a very simple yet high performance painting app. For many people this may be the last step needed to understand how to build a general painting app. However, there a few more concepts we can dive into to make the app better.

Creating smooth lines

If you run the app you’ll likely notice that all of the lines being drawn are very jagged. This is because we’re simply drawing straight lines between each touch point. Since touch events only fire about 50 times per second we need to use some interpolation to smooth out the lines in between those touch points.

We can solve this problem using bezier curves, but it comes at a cost. In order to know where to curve our lines to, we always need to calculate curves one frame behind the current touch point. In the example below you can see that the curve between touch 1 and touch 2 isn’t known until touch 3 is provided.

To create clean continuous lines we ultimately want to track a total of 4 touch points in order to create our control points for the line. I’m using the formula available at http://www.antigrain.com/research/bezier_interpolation/ to determine those control points, and feeding it into the code below.

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    point0 = CGPointMake(-1, -1);
    point1 = CGPointMake(-1, -1); // previous previous point
    point2 = CGPointMake(-1, -1); // previous touch point
    point3 = [touch locationInView:self]; // current touch point
}

- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    point0 = point1;
    point1 = point2;
    point2 = point3;
    point3 = [touch locationInView:self];
    [self drawToCache];
}

- (void) drawToCache {
    if(point1.x > -1){
        hue += 0.005;
        if(hue > 1.0) hue = 0.0;
        UIColor *color = [UIColor colorWithHue:hue saturation:0.7 brightness:1.0 alpha:1.0];

        CGContextSetStrokeColorWithColor(cacheContext, [color CGColor]);
        CGContextSetLineCap(cacheContext, kCGLineCapRound);
        CGContextSetLineWidth(cacheContext, 15);

        double x0 = (point0.x > -1) ? point0.x : point1.x; //after 4 touches we should have a back anchor point, if not, use the current anchor point
        double y0 = (point0.y > -1) ? point0.y : point1.y; //after 4 touches we should have a back anchor point, if not, use the current anchor point
        double x1 = point1.x;
        double y1 = point1.y;
        double x2 = point2.x;
        double y2 = point2.y;
        double x3 = point3.x;
        double y3 = point3.y;

        double xc1 = (x0 + x1) / 2.0;
        double yc1 = (y0 + y1) / 2.0;
        double xc2 = (x1 + x2) / 2.0;
        double yc2 = (y1 + y2) / 2.0;
        double xc3 = (x2 + x3) / 2.0;
        double yc3 = (y2 + y3) / 2.0;

        double len1 = sqrt((x1-x0) * (x1-x0) + (y1-y0) * (y1-y0));
        double len2 = sqrt((x2-x1) * (x2-x1) + (y2-y1) * (y2-y1));
        double len3 = sqrt((x3-x2) * (x3-x2) + (y3-y2) * (y3-y2));

        double k1 = len1 / (len1 + len2);
        double k2 = len2 / (len2 + len3);

        double xm1 = xc1 + (xc2 - xc1) * k1;
        double ym1 = yc1 + (yc2 - yc1) * k1;
        double xm2 = xc2 + (xc3 - xc2) * k2;
        double ym2 = yc2 + (yc3 - yc2) * k2;

        double smooth_value = 0.5;
        float ctrl1_x = xm1 + (xc2 - xm1) * smooth_value + x1 - xm1;
        float ctrl1_y = ym1 + (yc2 - ym1) * smooth_value + y1 - ym1;
        float ctrl2_x = xm2 + (xc2 - xm2) * smooth_value + x2 - xm2;
        float ctrl2_y = ym2 + (yc2 - ym2) * smooth_value + y2 - ym2;

        CGContextMoveToPoint(cacheContext, point1.x, point1.y);
        CGContextAddCurveToPoint(cacheContext, ctrl1_x, ctrl1_y, ctrl2_x, ctrl2_y, point2.x, point2.y);
        CGContextStrokePath(cacheContext);

        CGRect dirtyPoint1 = CGRectMake(point1.x-10, point1.y-10, 20, 20);
        CGRect dirtyPoint2 = CGRectMake(point2.x-10, point2.y-10, 20, 20);
        [self setNeedsDisplayInRect:CGRectUnion(dirtyPoint1, dirtyPoint2)];
    }
}

Now our app produces nice smooth lines that track relatively well to the intended path that the user naturally drew with their finger.

Adding multi-touch to this app is as simple as handling each individual touch inside the touchesMoved:(NSSet*)touches callback. Advanced drawing apps would likely move most of the drawing code to an OpenGL surface for even faster drawing, but that is beyond the scope of this example.

XCode Files:
Simple Painting with jagged lines
Advanced Painting with smooth lines

 

  • Sunho Park

    This is really top-most, greatest implementation and explanation about graphics especially on iOS.
    This is the greatest drawing implementation.

    Thanks a lot.
    And happy new year to you and your family.

  • Lee Barringer

    A really useful tutorial, thanks you! Just one very simple question … how are you setting the background colour in this example please? I’d like to adapt it a little – to white infact.

    Kind regards,

    Lee Barringer

  • Hi … do you have the Aobe Air code for this method? I am having a rough time in AIR achieving the finger tracking and smoothness.

    Thanks

  • Sean Christmann

    Lee, you can color the background when the cacheContext is irst created

    CGContextSetRGBFill(cacheContext, 1.0, 1.0, 1.0, 1.0);
    CGContextFillRect(cacheContext, self.bounds);

    this would set the background to white for example

    Ivan, I don’t have the code ready to distribute yet.

  • Pxr

    Any words on when the Android version will be online?
    I would be very interested 🙂

    Thanks for sharing!

  • Daniel

    Great example code, very much appreciated. I’m curious how you would fit in functionality like a rectangle drawing tool, where as you drag the rectangle redraws from start touch to touchmoved position and only stamping down on touchend. So the top buffer is cleared with each move until release. I was thinking of drawing in a new top layer and only adding to the bitmap on penup? I’m curious how you would think of adding something like that. In addition I’m thinking about undo/redo. I do have a drawing app that uses opengl and currently I’m simply saving the last 5 bitmaps in an array and swapping to the previous bitmap on undo. that actually works well and would work well with your code. Also curious if you have better ideas doing that. The main limitation for me is I can only keep a very limited number of undo bitmaps in memory as I’m not just saving vector shapes which would be much more light weight.

    Thanks again for any more pointers!

    Daniel

  • Crt

    There is an error in the code to change the background color. This one should do it:

    CGContextSetRGBFillColor(cacheContext, 1.0, 1.0, 1.0, 1.0);
    CGContextFillRect(cacheContext, self.bounds);

    Great code! Works like a charm.

    Thank you Sean:)

  • Andrej

    Thanks for the awesome tutorial. I want to implement a very similar app, except I want the drawing to be multioutch. The multitouch aspect is easy, but how would you keep the drawing efficient, now that you can’t simply get a small dirty area?

  • Joe

    Hi Sean

    Great piece of code. I tried changing the background and it does change. Is it possible to fill the rectangle with a picture from the camera roll?

    How would you clear your drawing?
    Thanks for any tips you might have on doing this.

    Joe

  • Abe

    Sean

    Great tutorial thanks. I modified the code for multitouch and it is mostly working. I am battling a little bit with a strange delay that happens with the first second or so of the drawing. Seems to be an iPad retina display problem.

    Thanks,
    Abe

  • MineS

    Hey~ I love you tutorial and I try to implement to my existing project.
    It looks pretty cool.

    But the only problem is if you test it on a retina display iPhone.
    The line is blurry, it’s probably we need to handle the *2 pixel by myself…

    But I don’t have any success on that, would you pls explain how?

    Thank you so much !

  • James

    Great job. There is one more problem. The first touch is kind of slow. — It will take sometime to show the initial path. Is there anyway to fix this? Thanks.

  • Abe

    @James. If you are experiencing the delay on the new iPad that is the same problem I was having. It is related to the cache context as I have tried code that omitted a cache and the delay went away. Best of luck.

  • Danilo de Castro

    Good afternoon.

    I’am creating an app to iOS and I have a doubt. How can I get an UIImage from PaintView?

    Thanks,

  • Waleed

    i want to change the bg color to white, how do i do that

    thanks a lot for this great tutorial

  • EffectiveUI Team

    Waleed, please read the other comments for the answer to your question about the background color. Thanks.

  • Hi… great example you have provided….

    I was trying to add background color to the paint view… but its not displayed, i tried using another view as background but that also did not worked… could you please help me how we can add the background view to this example???

  • Hi .. i tried to change the background color in initcontext method as suggested in the previous comments .. build is failing, is there any alternative to set the background color?

    Undefined symbols for architecture i386:
    “_CGContextSetRGBFill”, referenced from:
    -[PaintView initContext:] in PaintView.o
    ld: symbol(s) not found for architecture i386
    collect2: ld returned 1 exit status

    CGContextSetRGBFill(cacheContext, 1.0, 1.0, 1.0, 1.0);
    CGContextFillRect(cacheContext, self.bounds);

  • i was able to set the background color, using these two lines in initcontext method.

    CGContextSetRGBFillColor(cacheContext, 1.0, 1.0, 1.0, 1.0);
    CGContextFillRect(cacheContext, self.bounds);

  • i have to add a image on canvas which user can color?? is there any way we can add a image to paintview or we can add any background view to make this work??
    Please suggest.

  • Sajid

    Hi,
    Great tutorial!
    Could you please help us with how to make the background transparent? I am adding this view on anothr view with an image and the image should be visible when the user draw.

    Thanks
    Sajid

  • EffectiveUI Team

    Hi Sajid,

    Thanks for the comment. Please read the other comments on this post as they may have the answer to your question.

    Regards,
    Deb J.

  • Daniel

    I have the background coloring correctly but I cannot get it to be transparent. UIColor clearColor makes the PaintView instance black rather than clear. I was even able to show an image as the background but the transparent color built into the image is changed to black. Can you please help me with this?

  • Abhijit Sathe

    HI All,

    I am having the same problem. The first touch is kind of slow. — It will take sometime to show the initial path. Is there anyway to fix this? I didnt understand Abe’s comments saying
    “It is related to the cache context as I have tried code that omitted a cache and the delay went away.”
    Please tell me some way to remove this

    Thanks,
    Abhijit V Sathe

  • any update on parts 2 and 3?

  • Kim

    Hi

    Nice tutorial. I have a question though. How can I make the writing more “feathered”. As in like in bamboo paper and other note taking apps? Also, can I do anti-aliasing to the bitmap context?

  • Hey there,

    How would we go about laying an image onto the blank canvas and allow the user to paint over it? I’ve tried creating an UIImage with the resource bundled into the project and put in code in the drawRect method that does ‘[myPic drawInRect:rect];’ however not seeing any result, not even the image onto the screen. I also tried putting the image into a UIImageView which was then added to the PaintView and although the image was overlaid onto the screen, the painting was not visible when I attempted to draw over the image.

    Also, how would one go about implementing a clear method?

  • All,
    The first touches will be delayed if you are using the method to smooth the lines, that’s because we need a few touches before we know who to arc the lines being drawn. If you don’t want smooth lines that drop back to the code listed under “Next Step: Optimizing the Drawing Code”

    If you would like to draw an image behind the screen instad of trying to fill it black or white, you would use the following psuedo code after creating cacheContext

    CGContextDrawImage(cacheContext, self.bounds, [UIImage imageName:@”myimage.png”].CGImage);

    I haven’t tested this code exactly so I’m not sure if it will work exactly right, but you’ll want to look at CGContextDrawImage() to get it working. you might have to flip the cgrect due to the way Quartz2d does its drawing.

    I’m not sure about making the cacheContext transparent to begin with, you may need to draw the underlying view into this view cacheContext to acheive the results you are after. You would do this in drawRect with this code

    [self.viewToCopy.layer renderInContext:context];