iOS Programming Recipe 24: Creating a Mask for Clipping Drawings

This Recipe is inspired by a recent problem I ran into and had quite a bit of trouble with it. Basically I wanted to create a shape and fill it with a gradient. Sounds pretty easy huh? well, not quite as we’ll find out shortly. While my particular application required filling a circle with an ellipse, we can create any shape. In essence we’re creating a window (shaped however we want) where the user can see only a portion of the drawing behind it.

Assumptions

  • You have some familiarity with Xcode, if not, you should probably go do that here:
  • You have an understanding of pixels vs points. Refer to the documentation regarding the subject here: Drawing and Printing Guide for iOS

Setting Up the Project

To start off with create a single view application and make sure you remove the option for storyboards and choose iPhone under the devices dropdown.

Recipe24-1

Once the project is open we’ll need to create a new subclass of UIView and tie it to a view in our .xib file.
Click the plus sign in the lower left hand corner of the Xcode window and choose “New File” from the window. In the popup window, choose “Objective-C” class and press next.

Give the new class the name “DrawingView”. When everything is complete you should see a new .h and .m file in your Project Navigator.

Recipe24-2

Now open up the DrawingView.m file and take a peek at it:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "DrawingView.h"

@implementation DrawingView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
*/


@end

Go ahead and uncomment the drawRect: method. We’ll need this to do our drawing.

Now select the ViewController.xib file and drag a new view onto it from the Object Library on the bottom left hand corner of the Xcode screen. Resize the View to make it fill the entire screen.

Recipe24-3

With the view selected change the view class to “DrawingView” from the Identity inspector (OPT+CMD+3) in the top right hand corner of the screen.

Recipe24-4

Ok! Now were ready to start drawing some stuff!

Drawing an Ellipse

To start with, I want to have a frame of reference so I can point out an issue that occurs later in this tutorial. So we’ll start by drawing a black circle some where in the middle of the screen.

Create a new method as follows and place it right after the drawRect: method in the DrawingView inplementation file :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(void)drawEllipse:(CGContextRef)context{

    CGContextSaveGState(context);

    //Set color of current context
    [[UIColor blackColor] set];

    //Set shadow and color of shadow
    CGContextSetShadowWithColor(context, CGSizeZero, 10.0f, [[UIColor blackColor] CGColor]);

    //Draw ellipse
    CGRect ellipseRect = CGRectMake(60.0f, 150.0f, 200.0f, 200.0f);
    CGContextFillEllipseInRect(context, ellipseRect);

    CGContextRestoreGState(context);

}

This chunk of code first receives a context as an input. A context is where you can store drawing information like current color, stroke, and shadow properties.

The next piece CGContextSaveGState(context) saves the current context before making a change. At the end of this method we call another method CGContextRestoreGState. These are so we can draw an ellipse in any color and shadow without effecting any other items we draw after it. Without doing this, any new items we draw would be black and have a shadow. The important thing to note here is we are saving the state NOT the drawing.

Then we draw an Ellipse(Circle) at 60 points from the left and 150 points which has a 200 point diameter in both the X and Y directions.

Now we’ll want to call this method from within the drawRect: method.


1
2
3
4
5
6
7
8
9
10
- (void)drawRect:(CGRect)rect
{

    CGContextRef context = UIGraphicsGetCurrentContext();

    //call function to draw Ellipse
    [self drawEllipse:context];


}

This bit of code, first creates a new context. Then, we call our new method using that context. Build it and run it and you should see the following:

Recipe24-5

You have now been introduced to drawing with Core Graphics, of course there are many other things you can do, but I’m not gonna get into that now. Next we’ll create a gradient.

Creating the Gradient

Drawing gradients are actually kind of arduous unfortunately. For some things the effort pays off!

So we’re gonna go ahead and create a new method titled “drawGradient”, without further ado:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-(void)drawGradient:(CGContextRef)context{



    //Define start and end colors
    CGFloat colors [8] = {
        0.0, 0.0, 1.0, 1.0, //Blue
        0.0, 1.0, 0.0, 1.0 }; //Green

    //Setup a color space and gradient space
    CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace,     colors, NULL, 2);

    //Define the gradient direction
    CGPoint startPoint = CGPointMake(160.0f,0.0f);
    CGPoint endPoint = CGPointMake(160.0f, 400.0f);

    //Create and Draw the gradient
    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);

    CGColorSpaceRelease(baseSpace);
    CGGradientRelease(gradient);
}

Lot of steps for a simple little gradient huh?

As you can see, we first define our colors. We chose blue and green. Then you have to set up your colorspace and gradient space. This is just some ugly boilerplate code that should be included in order for things to happen. we want the gradient to start at the top middle and move down 400 points. The last step is just drawing it.

Now, go ahead and call this function from the drawRect: method.


1
2
3
4
5
6
7
8
9
10
11
12
- (void)drawRect:(CGRect)rect
{

    CGContextRef context = UIGraphicsGetCurrentContext();

    //call function to draw Ellipse
    [self drawEllipse:context];

    //drawGradient
    [self drawGradient:context];  

}

Go ahead and run it and you should see the following:

Recipe24-6

The problem here is we just covered up our ellipse. Lets clip this to a window.

Clipping the Gradient Using a Drawing

To clip a gradient (or for that matter any drawing) using another drawing, we first have to create that drawing or mask. so let’s get to it.

We’ll start by adding a new method drawEllipseWithGradient and expanding on it. So here is the part necessary to create the mask image:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-(void)drawEllipseWithGradient:(CGContextRef)context{

    CGContextSaveGState(context);

    //UIGraphicsBeginImageContextWith(self.frame.size);
    UIGraphicsBeginImageContextWithOptions((self.frame.size), NO, 0.0);

    CGContextRef newContext = UIGraphicsGetCurrentContext();  

    //Set color of current context
    [[UIColor blackColor] set];

    //Draw ellipse <- I know we’re drawing a circle, but a circle is just a special ellipse.
    CGRect ellipseRect = CGRectMake(110.0f, 200.0f, 100.0f, 100.0f);
    CGContextFillEllipseInRect(newContext, ellipseRect);

    CGImageRef mask = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext());
    UIGraphicsEndImageContext();


    CGImageRelease(mask);

}
  • Alright! so what we have done here is first we save our state so this doesn’t effect the and new drawings.
  • Then we have to create a new image context and a new graphics context.
  • We draw a black ellipse at 110 points from the left and 200 points down. then we * create a bitmap from our drawing of the ellipse.

Now we have our mask, lets define the clipping region using it.

Go ahead and add this line fo code right after CGImageRef mask…


1
CGContextClipToMask(context, self.bounds, mask);

Ok, we’re almost there. everything we draw from now on will be clipped by this ellipse we have created. So rather than calling the drawGradient method from the drawRect: method. It makes more sense to call it from here.

Go ahead and complete the drawEllipseWithGradient method as follows:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-(void)drawEllipseWithGradient:(CGContextRef)context{

    CGContextSaveGState(context);

    //UIGraphicsBeginImageContextWith(self.frame.size);
    UIGraphicsBeginImageContextWithOptions((self.frame.size), NO, 0.0);

    CGContextRef newContext = UIGraphicsGetCurrentContext();  

    //Set color of current context
   [[UIColor blackColor] set];

   //Draw ellipse <- I know we’re drawing a circle, but a circle is just a special ellipse.
   CGRect ellipseRect = CGRectMake(110.0f, 200.0f, 100.0f, 100.0f);
   CGContextFillEllipseInRect(newContext, ellipseRect);

    CGImageRef mask = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext());
   UIGraphicsEndImageContext();


    CGContextClipToMask(context, self.bounds, mask);


    [self drawGradient:context];
    CGImageRelease(mask);
    CGContextRestoreGState(context);


}

Ok, you should be able to go ahead and run it:

Recipe24-7

Notice anything? Yeah, this is where this gets tricky. The coordinates we gave for the new smaller circle should have been centered with respect to the bigger circle. The reason it’s not is because we had to draw the smaller circle using quartz 2D. Quartz 2D starts from the bottom left instead of the top left. So our coordinates are upsidedown. This convention predates iOS.

To fix this we need to add a little bit of code to translate and scale the image context to match the top left coordinate system.

Now go ahead and update the drawEllipseWithGradient method once again as follows:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
-(void)drawEllipseWithGradient:(CGContextRef)context{

    CGContextSaveGState(context);

    //UIGraphicsBeginImageContextWith(self.frame.size);
    UIGraphicsBeginImageContextWithOptions((self.frame.size), NO, 0.0);

    CGContextRef newContext = UIGraphicsGetCurrentContext();

    // Translate and scale image to compnesate for Quartz's inverted coordinate system
    CGContextTranslateCTM(newContext,0.0,self.frame.size.height);
    CGContextScaleCTM(newContext, 1.0, -1.0);



    //Set color of current context
    [[UIColor blackColor] set];

    //Draw ellipse <- I know we’re drawing a circle, but a circle is just a special ellipse.
    CGRect ellipseRect = CGRectMake(110.0f, 200.0f, 100.0f, 100.0f);
    CGContextFillEllipseInRect(newContext, ellipseRect);

    CGImageRef mask = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext());
    UIGraphicsEndImageContext();


    CGContextClipToMask(context, self.bounds, mask);


    [self drawGradient:context];
    CGImageRelease(mask);
    CGContextRestoreGState(context);


}

so we added two lines of code there. First we use CGContextTranslateCTM to translate the image context upward the full height of the frame.

Then use CGContextScaleCTM to flip the coordinates upside down.

Now if you build and run it, all should be dandy!

Recipe24-8

You can find the source for this recipe on our github page at:

About Joe Hoffman

I am An Electrical Engineer that spends a lot of my off time doing web development and other programming. Currently I am trying to expand my knowledge with iOS and I find that writing tutorials helps me learn more thoroughly as well as provide some useful info to others.

Speak Your Mind

*

css.php
Privacy Policy