A crash course on CoreGraphics

Core Graphics (CG), a.k.a. Quartz, is an old API included in both Cocoa and Cocoa Touch. It allows you to draw paths, shapes, shadows and gradients on the so-called graphic destinations or Graphic Contexts (we will come back later on this concept).

The idea behind CG is very simple. If you want to modify any view you have to subclass it and override its drawRect: method. Every view has a drawRect: method. So, if you want to customize a view you need to overwrite that method adding your code there, combining paths, shadows and gradients. You can do this with any UIView subclass. In this way, you can customize buttons, labels, maps and, in general, any graphic object descendant of UIView. In a similar way, you can use CG “to paint” on an image or on a PDF file.

You could tell me: “Hey, just a moment! Why should I write a bunch of code, if I have my graphic designer or if I can create my graphics using powerful tools like Photoshop?” Well, that’s right, you don’t need to use CG code to draw everywhere. Moreover, trying to reproduce simple Photoshop effects could be really hard to achieve and would require many lines of code.

However, there are some advantages in using CG instead of importing your very nice prepared graphics. For example, if you need to draw something in realtime that changes dynamically its visual appearance (a rounded square changing its color according to the sound input of the microphone), then you should think of using CG (yeah, you can also use Core Animation for this particular case).

Additionally, CG reduces the application memory footprints (think of the new iPad with the Retina Diaplay). Including every graphic object in your application increases the size of the application bundle. For the retina devices, this size can be quite significative. So, instead of loading images from the memory, you could use CG to draw at runtime your objects.

Another significant advantage of using CG instead of a pre-loaded graphic object is the resolution independence. This means the same source code can run on the iPhone with and without the retina display, on the iPad with and without the retina display and on the Mac with or without HiDPI. All this, with no need to create the same object in three or four different resolutions. Additionally, when you scale an image at runtime, the aliasing effect can make your object looking ugly. CG re-renders the object without this problem.

Now, before we get into the details on how to draw on a view using CG, I need to review with you some basic concepts. To understand CG, you need to understand three basics things: the Graphic Context, the Path and the Graphic State. But, even before that, we need go through how CG “paints” on a view.

There are two approaches: Painter Model and Crayon Model. The Painter Model is the way an artist paints on a canvas. Artists use to draw the background of a scene first and then proceed adding the foreground objects on top of each others until the top-front foreground object is drawn. When we will start using CG, we will follow the same approach. For example, to draw a simple square with a dark shadow in the background, we need to tell CG that you need a shadow with a given shape, color and blur and then tell CG about the foreground square.
This is the sequence to draw a complex set of graphic objects. But what about each single object? In this case, we will use the Crayon Model. Kids usually use this method when painting something on a piece of paper. They usually stroke a shape and than they fill it with some colors. Using CG, you will follow the same approach: you define the shape of an object, first and then you have to color it.

Ok, let’s go now through the above mentioned basic concepts: Graphic Context, Path and Graphic State.

Graphic Context

The Graphic Context is your graphic destination. If you want to draw on a view, the view is your Graphic Context. Instead, if you want to draw on an image, the image is your Graphic Context. So, the first thing you should do, before trying to draw anything is to define the Graphic Context. Each Core Graphics function has a pointer to the current graphic context.

You get the current Graphic Context by using the UIGraphicsGetCurrentContext function (Yes, I said function and not method. This is because CG is a C-based API. Sorry, no ObjC). So, after subclassing your UIView, you override the drawRect: method (don’t forget to execute the method on super) and get the current graphic context. In this way:

Now that we have a reference to the current Graphic Context, we could start to draw.

Path

Each drawing is composed of some basic graphic elements: points, lines, arcs. All of them are referred to as Paths. A path is a set of lines, arcs and curves you can draw on the current graphic context to build complex objects. Only when the path description is completed, you can draw the path on the screen and choose to fill it or stroke it or both.

Core Graphics provides you with specific functions to draw paths. Some of these functions are specific and can be used to draw a line, a rectangle, an ellipse, an arc, etc.

Graphic State

Before we move to a practical example, the last concept you should know is related on how Quartz draws on the graphic destination. The Graphic State keeps track of the colors, the size of the path, the transformations and many other features you want to apply to a path or group of paths.

States are maintained in stack. You save the state using CGContextSaveCGState. In this way, you push the graphic state onto the stack. When you want to reload the previous graphic state, you use CGContextRestoreCGState. In this way, the state is popped from the stack.

Hands-on

Let’s see now how to prepare our development environment. Let’s create a basic iOS project. I prefer to use the iPhone template for this exercise, but feel free to use the iPad template, if you prefer.

Launch Xcode and create a Single-View Application project. Call it DigitalPicasso and use ARC. Once the project window opens, open the ViewController.xib file. This view controller has an associated view that we are going to use as our Graphic Context. To do so, we’d need to override its drawRect: method as it has been previously discussed. Select the view in the ViewController.xib file and change the Class field to MyView in the Indentity Inspector.

Now, let’s add the class files. Select File -> New -> Files.... In the window panel, select Cocoa Touch -> Objective-C class and press the Next button. Give “MyClass” as name and UIView as “Subclass of”. Click Next and save the files in the DigitalPicasso folder.

Drawing paths

Now, you have two new files in the Xcode project (MyView.h and MyView.m). Open the MyView.m and add the following code:

Now, just Run. If everything was done correctly, you get the same result shown in the following figure.

Example 1

Let’s give a look at what we have done. After executing the drawRect: method on super, we get the current Graphic Context using the UIGraphicsGetCurrentContext function. The pointer ctx is very important, because we need to pass it as first argument to the Core Graphics functions. After this, we tell CG we want to begin a new path. Take into account that nothing is drawn on the screen until the last function CGContextStrokePath is executed. The rest of the code is just a preparation. CGContextMoveToPoint is used to place your pencil on a selected point of the screen. After that, you start to add lines to the path, using the CGContextAddLineToPoint until you finally close the path. Using the CGContextClosePath you connect the last point of your path with the first one. For Core Graphics a path can be a line, a rectangle, an arc or an ellipse. Once you have create a path, you can either stroke it or fill in or both. Here, I write for you some functions that could be useful to create a path. Give a look at them and check the documentation. In the same doc page, you will find other useful functions organized by type.

Let’s play a little bit with these functions. After the CGContextStrokePath function, add the following lines of code:

Build and Run. Cool, right? Now, try to use the previous functions. Next time, I will show you how to create reusable paths and change the colors of your pencil.

Example 2

Again on the Graphic States

The only problem we have now is that everything you try to draw on the screen has the same color, the same pencil size and type. A little bit borying, right? This means that everything is in the same graphic state. Obviuosly, we can modify this. When you change anything related with the status of your environment (colors, type of line, line width and so on), it is a good idea to save the current state before applying any change. In this way, you can always go back without redefining all the possible features. To store the current graphic state you use the CGContextSaveCGState function. Instead, use CGContextRestoreCGState is used to to revert to the previous state. Graphic states are stored in a LIFO (Last In - First Out) stack. So, every time you save a state, this is added onto the stack. If you save 2 times the state, if you want to restore the first state, you need to restore the state 2 times. If you need to go back to the previous state, then you simply restore the previous state using the CGContextRestoreCGState function.

Colors

Let’s try to save the state, change the pen colors and revert to the previous state. Before that, let’s talk a little bit about how to manage colors in Core Graphics. There is a set of Core Graphics functions you can use to modify the color of your pen. However, I suggest you to use UIKit as much as you can, since UIKit has very easy methods to create colors. You can then use the CGColor method to convert a UIColor to a CGColor. For example, you can simply use this line of code to create a red CG color:

This is the most convenient way to create a CG color. The other approach is to use one of the following Core Graphics functions:

So, let’s modify our DigitalPicasso project in this way.

You should get something similar to this:

Example 3

Now, the problem is that if you want to go back to the black pen, you need to set again the black color. In this case, it is very simple, you need just to add a line of code. But what happens if you want to change again to many other pen features? Here where the states help you. So, before changing the black pen to a white one, as we did in the previous code, we should save the current state. Give a look at the following code:

So, we saved the state and then, we restored it. And this is the result:

Example 4

Now, you see the new circle is again a black stroke, because we simply restored the previous CG state.

Extra settings

Beside the color you can also change other pen features. For example, you can change the pen size using CGContextSetLineWidth. Add this line, before line 20 of the previous code.

You get a thicker circle stroke as in the following figure. You can also change the transparency of your pen, using CGContextSetAlpha. Try this code and see what happens:

Reusable paths

Reusable paths are very interesting Core Graphics tools, since they can help you saving a lot of time when you try to draw complex objects on the screen. A reusable path is a path you create just once and then, you can quickly and easily apply it later. Using reusable paths is very simple. You need to use CGPath functions in place of the CGContext ones. First, create a CGPathRef using the CGPathCreateMutable function. Then, add paths to the CGPathRef and finally close the path using CGPathCloseSubpath. To draw the path in a context you need to use CGContextAddPath. The following table shows the equivalence between reusable and not-reusable path functions:

So, you can now try to convert the code we wrote untill now to use resuable paths.

As you can see in the above example, I created a new reusable path. Later, whenever I need to draw that path again, I simply use the reference myPath. Reusable paths are very useful when you want to draw different layers with the same shape. For example, if you want to create a square with a shadow, you need first to create a square shadow (do you remember the Crayon model?) and then, draw your square path on top of the shadow. If you use reusable paths, this is very easy: you just need to create the reusable square and draw it once as a shadow (I will show you later how to do that) and once again as a squared shape.

Color Gradients

A color gradient is a smooth transition from any color to a different one. This transition makes your drawings more realistic and look gorgeous. There are essentially 2 ways to draw a gradient in Core Graphics. You can either use the CGGradientRef object or the CGShadingRef object. The first approach is very simple and allows you to draw linear gradients. The second approach is a bit more complex, since the user has to specify a CGFunctionRef object to draw a gradient with any shape. So, to draw a color gradient with CG, you need to:

  • Define a color space
  • Define the list of colors you want to draw
  • Define the locations where you want the colors appear
  • Draw (finally) your gradient; and, finally
  • Free up your memory

Color space

So, first let’s give a look at the color spaces. A color space specifies how color values are interpreted by the graphic engine. A color space is usually a multi-dimensional space, each dimension of which represents a specific color component. Defining a color space in Core Graphics is quite straightforward. Give a look at the class CGColorSpace. I usually use the CGColorSpaceCreateDeviceRGB function, but you can use any of the other functions provided in the class.

Memory management

Just a small interruption here, since I missed an important point until now. The memory management in Core Graphics follows the simple rules of the retain count mechanism. Sorry, no ARC. You would ask me: “Ehy, wait a minute didn’t you say CG is C-based? So, no objects?”. You are right. And indeed, we are not using objects. However, it is really common to call objects the CG references. These are the variables of a type ending with word Ref. For example, when you create a color space, you create a reference (a pointer) of type CGColorSpaceRef. This is simply a pointer, but we usually call it an object. Now, whenever you create a CG object (or in general, a Core Foundation object) with a function whose name contains the word Create or Copy, you are responsible to release that object. In general, you can use the Core Foundation function, CFRelease. However, in the case of the color space, there is a specialized function for that: CGColorSpaceRelease. So, if you create a color space with the above mentioned functions, you need to release it as show in this chunk of code:

Gradient example

So, let’s see how we can create a linear gradient, first. Let’s color the whole screen. So, open Xcode, create a new View-based project, create a new class subclassing UIView and add a view to the main view of your custom view controller. In the MyView class, edit the drawRect: method as follows:

So, what is this chunk of code? First, we get the reference of the current graphic context. Then, we create a reference to the color space. I decided to use the RGB color space. This multi-dimentional space has 4 components: Red, Green, Blue and Alpha (the transparency). Each component has a value between 0 and 1. After creating the color space, we create the color components. This is a vector containing the 2 or more colors you want to use in your gradient. In the above example, I used just 3 colors. The first component is (R, G, B, A) = (1, 0, 0, 1). The second component is (R, G, B, A) = (0, 0, 0, 1). And the third component is (R, G, B, A) = (0, 1, 0, 1).

Now, we need to tell CG where to draw these colors. To draw the linear gradient, we use the CGContextDrawLinearGradient function. This function asks to specify the 2 points in the view where to the gradient starts and ends. You can see I created 2 CGPoints. Now, imagine to connect these 2 points with a segment. The aim of the locations vector is to define the percentage of that segment where the components are placed along it. In the above example, the first color is at the 0% of the segment (i.e., the beginning), the second component is at the 50% (just in the middle of the segment) and the last color component is just at the end of the segment. If you now run your project, you should get this:

Example 5

Very cool, eh? Now, imagine to combine this with shadow and transparencies. You can make really cool stuffs. Try to play a little bit with this and apply it to the paths. Our approach is to combine the graphics prepared by our graphic designer and the real time drawing of Core Graphics. You can make really beautiful pieces of arts.

iOS Consulting | INVASIVECODE

iOS Training | INVASIVECODE

(Visited 37 times, 1 visits today)