Views and the View Hierarchy - iOS Programming: The Big Nerd Ranch Guide (2014)

iOS Programming: The Big Nerd Ranch Guide (2014)

4. Views and the View Hierarchy

In this chapter, you will learn about views and the view hierarchy. In particular, you are going to write an app named Hypnosister that draws a full-screen set of concentric circles.

Figure 4.1 Hypnosister

Hypnosister

In Xcode, select File → New → Project... (or use the keyboard shortcut Command-Shift-N). From the iOS section, select Application, choose the Empty Application template, and click Next.

Enter Hypnosister for the product name and BNR for the class prefix, as shown in Figure 4.2. Make sure iPhone is selected from the Devices drop down and make sure the Use Core Data box is unchecked.

Figure 4.2 Configuring Hypnosister

Configuring Hypnosister

Hypnosister will not have any user interaction so that you can focus on how views are drawn to the screen. Let’s start with a little theory of views and the view hierarchy.

View Basics

· A view is an instance of UIView or one of its subclasses.

· A view knows how to draw itself.

· A view handles events, like touches.

· A view exists within a hierarchy of views. The root of this hierarchy is the application’s window.

In Chapter 1, you created four views for the Quiz app: two instances of UIButton and two instances of UILabel. You created and configured these views in Interface Builder, but you can also create views programmatically. In Hypnosister, you will create views programmatically.

The View Hierarchy

Every application has a single instance of UIWindow that serves as the container for all the views in the application. The window is created when the application launches. Once the window is created, you can add other views to it.

When a view is added to the window, it is said to be a subview of the window. Views that are subviews of the window can also have subviews, and the result is a hierarchy of view objects with the window at its root.

Figure 4.3 An example view hierarchy and the interface that it creates

An example view hierarchy and the interface that it creates

Once the view hierarchy has been created, it will be drawn to the screen. This process can be broken into two steps:

· Each view in the hierarchy, including the window, draws itself. It renders itself to its layer, which is an instance of CALayer. (You can think of a view’s layer as a bitmap image.)

· The layers of all the views are composited together on the screen.

Figure 4.4 shows another example view hierarchy and the two drawing steps.

Figure 4.4 Views render themselves and then are composited together

Views render themselves and then are composited together

Classes like UIButton and UILabel already know how to render themselves to their layers. For instance, in Quiz, you created instances of UILabel and told them what text to display, but you did not have to tell them how to draw text. Apple’s developers took care of that.

Apple, however, does not provide a class whose instances know how to draw concentric circles. Thus, for Hypnosister, you are going to create your own UIView subclass and write custom drawing code.

Subclassing UIView

To create a UIView subclass, select File → New → File... (or press Command-N). From the iOS section, select Cocoa Touch and then choose Objective-C class (Figure 4.5).

Figure 4.5 Creating a new class

Creating a new class

Click Next. On the next pane, name the class BNRHypnosisView and select UIView as the superclass.

Figure 4.6 Choosing UIView as the superclass

Choosing UIView as the superclass

Click Next. Make sure that Hypnosister is checked beside Targets and then click Create.

Before writing the concentric circle drawing code for BNRHypnosisView, let’s focus on how to create a view programmatically and get it on screen. To keep things simple, in this first part, an instance of BNRHypnosisView view will not draw concentric circles. Instead, it will draw a rectangle with a red background.

Figure 4.7 Initial version of BNRHypnosisView

Initial version of BNRHypnosisView

Views and frames

Open BNRHypnosisView.m. The UIView subclass template has provided two method stubs for you. The first is initWithFrame:, the designated initializer for UIView. This method takes one argument, a CGRect, that will become the view’s frame, a property on UIView.

@property (nonatomic) CGRect frame;

A view’s frame specifies the view’s size and its position relative to its superview. Because a view’s size is always specified by its frame, a view is always a rectangle.

A CGRect contains the members origin and size. The origin is a C structure of type CGPoint and contains two float members: x and y. The size is a C structure of type CGSize and has two float members: width and height (Figure 4.8).

Figure 4.8 CGRect

CGRect

Open BNRAppDelegate.m. At the top of this file, import the header file for BNRHypnosisView.

#import "BNRAppDelegate.h"

#import "BNRHypnosisView.h"

@implementation BNRAppDelegate

In BNRAppDelegate.m, find the template’s implementation of application:didFinishLaunchingWithOptions:. After the line that creates the window, create a CGRect that will be the frame of a BNRHypnosisView. Next, create an instance of BNRHypnosisView and set its backgroundColor property to red. Finally, add the BNRHypnosisView as a subview of the window to make it part of the view hierarchy.

- (BOOL)application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

CGRect firstFrame = CGRectMake(160, 240, 100, 150);

BNRHypnosisView *firstView = [[BNRHypnosisView alloc] initWithFrame:firstFrame];

firstView.backgroundColor = [UIColor redColor];

[self.window addSubview:firstView];

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

A structure is not an Objective-C object, so you cannot send messages to a CGRect. To create one, you used CGRectMake() and pass in the values for the origin.x, origin.y, size.width and size.height. A CGRect is small compared to most objects, so instead of passing a pointer to it, you just pass the entire structure. Thus, initWithFrame: expects a CGRect, not a CGRect *.

To set the backgroundColor, you used the UIColor class method redColor. This is a convenience method; it allocates and initializes an instance of UIColor that is configured to be red. There are a number of UIColor convenience methods for common colors, such as blueColor, blackColor, andclearColor.

Build and run the application. The red rectangle is the instance of BNRHypnosisView. Because the BNRHypnosisView’s frame’s origin is (160, 240), its top left corner is 160 points to the right and 240 points down from the top-left corner of the window (its superview). The view stretches 100 points to the right and 150 points down from its origin, in accordance with its frame’s size.

Figure 4.9 Hypnosister with one BNRHypnosisView

Hypnosister with one BNRHypnosisView

Note these values are in points, not pixels. If the values were in pixels, then they would not be consistent across displays of different resolutions (i.e., Retina vs. non-Retina). A single point is a relative unit of a measure; it will be a different number of pixels depending on how many pixels there are in the display. Sizes, positions, lines, and curves are always described in points to allow for differences in display resolution.

On a Retina Display, a pixel is half a point tall and half a point wide by default. On a non-Retina Display, one pixel is one point tall and one point wide by default. When printing to paper, an inch is 72 points long.

In Xcode’s console, notice the comment informing you that “Application windows are expected to have a root view controller at the end of application launch.” A view controller is an object that controls some set of an application’s view hierarchy, and most iOS apps have one or more view controllers. Hypnosister, however, is simple enough that it does not need a view controller, so you can ignore this comment. You will learn about view controllers in Chapter 6.

Take a look at the view hierarchy that you have created:

Figure 4.10 UIWindow has one subview – a BNRHypnosisView

UIWindow has one subview – a BNRHypnosisView

Every instance of UIView has a superview property. When you add a view as a subview of another view, the inverse relationship is automatically established. In this case, the BNRHypnosisView’s superview is the UIWindow. (To avoid a strong reference cycle, the superview property is a weak reference.)

Let’s experiment with your view hierarchy. In BNRAppDelegate.m, create another instance of BNRHypnosisView with a different frame and background color.

...

[self.window addSubview:firstView];

CGRect secondFrame = CGRectMake(20, 30, 50, 50);

BNRHypnosisView *secondView = [[BNRHypnosisView alloc] initWithFrame:secondFrame];

secondView.backgroundColor = [UIColor blueColor];

[self.window addSubview:secondView];

self.window.backgroundColor = [UIColor whiteColor];

...

Build and run again. In addition to the red rectangle, you will see a blue square near the top lefthand corner of the window. Figure 4.11 shows the updated view hierarchy.

Figure 4.11 UIWindow has two subviews as siblings

UIWindow has two subviews as siblings

A view hierarchy can be deeper than two levels. Let’s make that happen by adding the second instance of BNRHypnosisView as a subview of the first instance of BNRHypnosisView instead of the window:

Figure 4.12 One BNRHypnosisView as a subview of the other

One BNRHypnosisView as a subview of the other

In BNRAppDelegate.m, make this change.

...

BNRHypnosisView *secondView = [[BNRHypnosisView alloc] initWithFrame:secondFrame];

secondView.backgroundColor = [UIColor blueColor];

[self.window addSubview:secondView];

[firstView addSubview:secondView];

...

Build and run the application. Notice that secondView’s position on the screen has changed. A view’s frame is relative to its superview so the top-left corner of secondView is now inset (20, 30) points from the top-left corner of firstView.

Figure 4.13 Hypnosister with new hierarchy

Hypnosister with new hierarchy

(If the blue instance of BNRHypnosisView looks smaller than it did previously, that is just an optical illusion. Its size has not changed.)

Now that you have had some experience with the view hierarchy, remove the second instance of BNRHypnosisView before continuing.

...

[self.window addSubview:firstView];`

CGRect secondFrame = CGRectMake(20, 30, 50, 50);

BNRHypnosisView *secondView = [[BNRHypnosisView alloc] initWithFrame:secondFrame];

secondView.backgroundColor = [UIColor blueColor];

[view addSubview:secondView];

self.window.backgroundColor = [UIColor whiteColor];

...

Custom Drawing in drawRect:

So far, you have subclassed UIView, created instances of the subclass, inserted them into the view hierarchy, and specified their frames and backgroundColors. In this section, you will write the custom drawing code for BNRHypnosisView in its drawRect: method.

The drawRect: method is the rendering step where a view draws itself onto its layer. UIView subclasses override drawRect: to perform custom drawing. For example, the drawRect: method of UIButton draws light-blue text centered in a rectangle.

The first thing that you typically do when overriding drawRect: is get the bounds rectangle of the view. The bounds property, inherited from UIView, is the rectangle that defines the area where the view will draw itself.

Each view has a coordinate system that it uses when drawing itself. The bounds is a view’s rectangle in its own coordinate system. The frame is the same rectangle in its superview’s coordinate system.

You might be wondering, “Why do we need another rectangle when we already have frame?”

The frame and bounds rectangles have distinct purposes. A view’s frame rectangle is used during compositing to lay out the view’s layer relative to the rest of the view hierarchy. The bounds rectangle is used during the rendering step to lay out detailed drawing within the boundaries of the view’s layer. (Figure 4.14).

Figure 4.14 bounds vs. frame

bounds vs. frame

You can use the bounds property of the window to define the frame for a full-screen instance of BNRHypnosisView.

In BNRAppDelegate.m, update firstView’s frame to match the bounds of the window.

- (BOOL)application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

// Override point for customization after application launch

CGRect firstFrame = CGRectMake(160, 240, 100, 150);

CGRect firstFrame = self.window.bounds;

BNRHypnosisView *firstView = [[BNRHypnosisView alloc] initWithFrame:firstFrame];

[self.window addSubview:firstView];

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

Build and run the application, and you will be greeted with a full-sized view with a red background.

Drawing a single circle

You are going to ease into the drawing code by drawing a single circle – the largest that will fit within the bounds of the view.

In BNRHypnosisView.m, add code to drawRect: that finds the center point of bounds.

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// Figure out the center of the bounds rectangle

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

}

Next, set the radius for your circle to be half of the smaller of the view’s dimensions. (Determining the smaller dimension will draw the right circle in portrait and landscape orientations.)

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// Figure out the center of the bounds rectangle

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

// The circle will be the largest that will fit in the view

float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);

}

UIBezierPath

The next step is to draw the circle using the UIBezierPath class. Instances of this class define and draw lines and curves that you can use to make shapes, like circles.

First, create an instance of UIBezierPath.

- (void)drawRect:(CGRect)rect

{

...

// The circle will be the largest that will fit in the view

float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);

UIBezierPath *path = [[UIBezierPath alloc] init];

}

The next step is defining the path that the UIBezierPath object should follow. How do you define a circle-shaped path? The best place to find an answer to this question is the UIBezierPath class reference in Apple’s developer documentation.

Using the developer documentation

From Xcode’s menu, select Help → Documentation and API Reference. You can also use the keyboard shortcut Option-Command-? (be sure to hold down the Shift key, too, to get the ‘?’).

(When you access the documentation, Xcode may try to go get the latest for you from Apple. You may be asked for your Apple ID and password.)

When the documentation browser opens, search for UIBezierPath. You will be offered several results. Find and select UIBezierPath Class Reference.

Figure 4.15 Documentation results

Documentation results

This page opens to an overview of the class, which is interesting, but let’s stay focused on your circle-shaped path question. The lefthand side of the reference is the table of contents. (If you do not see a table of contents, select the Documentation results icon at the top left of the browser.)

In the table of contents, find the Tasks section. This is a good place to begin the hunt for a method that does something specific. The first task is Creating a UIBezierPath Object. You have already done that, so take a look at the second task: Constructing a Path. Select this task, and you will see a list of relevant UIBezierPath methods.

Figure 4.16 Methods for constructing a path

Methods for constructing a path

A likely candidate for a circular path is addArcWithCenter:radius:startAngle:endAngle:clockwise:. Click this method to see more details about its parameters. You have already computed the center and the radius. The start and end angle values are in radians. To draw a circle, you will use 0 for the start angle and M_PI * 2 for the end angle. (If your trigonometry is rusty, you can take our word on this or click the Figure 1 link within the Discussion of this method’s documentation to see a diagram of the unit circle.) Finally, because you are drawing a complete circle, the clockwise parameter will not matter. It is a required parameter, however, so you will need to give it a value.

In BNRHypnosisView.m, send a message to the UIBezierPath that defines its path.

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// Figure out the center of the bounds rectangle

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

// The circle will be the largest that will fit in the view

float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);

UIBezierPath *path = [[UIBezierPath alloc] init];

// Add an arc to the path at center, with radius of radius,

// from 0 to 2*PI radians (a circle)

[path addArcWithCenter:center

radius:radius

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

}

You have defined a path, but you have not drawn anything yet. Back in the UIBezier class reference, find and select the Drawing Paths task. From these methods, the best choice is stroke. (The other methods either fill in the entire shape or require a CGBlendMode that you do not need.)

In BNRHypnosisView.m, send a message to the UIBezierPath that tells it to draw.

- (void)drawRect:(CGRect)rect

{

...

UIBezierPath *path = [[UIBezierPath alloc] init];

// Add an arc to the path at center, with radius of radius,

// from 0 to 2*PI radians (a circle)

[path addArcWithCenter:center

radius:radius

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

// Draw the line!

[path stroke];

}

Build and run the application, and you will see a thin, black outline of a circle that is as wide as the screen (or as tall if you are in landscape orientation).

Figure 4.17 BNRHypnosisView with a single circle

BNRHypnosisView with a single circle

Based on the original plan for Hypnosister, the line describing your circle is not yet right. It should be wider and light gray.

To see how to fix these issues, return to the UIBezierPath reference. In the table of contents, find the Properties section. One of these properties should stand out as useful in this case – lineWidth. Select this property. You will see that lineWidth is of type CGFloat and that its default is 1.0.

In BNRHypnosisView.m, make the width of the line 10 points.

- (void)drawRect:(CGRect)rect

{

...

// Add an arc to the path at center, with radius of radius,

// from 0 to 2*PI radians (a circle)

[path addArcWithCenter:center

radius:radius

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

// Configure line width to 10 points

path.lineWidth = 10;

// Draw the line!

[path stroke];

}

Build and run the application to confirm that the line is now wider.

There is no property in UIBezierPath that deals with the color of the line. But there is a clue in the class overview. Use the table of contents to return to the Overview. In the fifth paragraph (as of this writing), there is a parenthetical aside that reads, “You set the stroke and fill color using theUIColor class.”

The UIColor class is linked, so you can click it to be taken directly to the UIColor class reference. In UIColor’s Tasks section, select Drawing Operations and browse through the associated methods. For your purposes, you could use either set or setStroke. You will use setStroke to make your code more obvious to others.

The setStroke method is an instance method, so you need an instance of UIColor to send it to. Recall that UIColor has convenience methods that return common colors. You can see these methods listed under the Class Methods section of the UIColor reference, including one named lightGrayColor.

Now you have the information you need. In BNRHypnosisView.m, add code to create a light gray UIColor instance and send it the setStroke message so that when the path is drawn, it will be drawn in light gray.

- (void)drawRect:(CGRect)rect

{

...

// Configure line width to 10 points

path.lineWidth = 10;

// Configure the drawing color to light gray

[[UIColor lightGrayColor] setStroke];

// Draw the line!

[path stroke];

}

Build and run the application, and you will see a wider, light gray outline of a circle.

By now, you will have noticed that a view’s backgroundColor is drawn regardless of what drawRect: does. Often, you will set the backgroundColor of a custom view to be transparent, or “clear-colored,” so that only the results of drawRect: show.

In BNRAppDelegate.m, remove the code that sets the background color of the view.

BNRHypnosisView *firstView = [[BNRHypnosisView alloc] initWithFrame:firstFrame];

firstView.backgroundColor = [UIColor redColor];

[self.window addSubview:view];

Then, in BNRHypnosisView.m, add code to initWithFrame: to set the background color of every BNRHypnosisView to clear.

- (instancetype)initWithFrame:(CGRect)frame

{

self = [super initWithFrame:frame];

if (self) {

// All BNRHypnosisViews start with a clear background color

self.backgroundColor = [UIColor clearColor];

}

return self;

}

Build and run the application. Figure 4.18 shows the clear background and the resulting circle.

Figure 4.18 BNRHypnosisView with clear background

BNRHypnosisView with clear background

Drawing concentric circles

There are two approaches you can take to draw multiple concentric circles inside the BNRHypnosisView. You can create multiple instances of UIBezierPath, each one representing one circle. Or you can add multiple circles to the single instance of UIBezierPath, and each circle will be a sub-path. It is slightly more efficient to use one instance, so you are going to do that.

To fill the screen with concentric circles, you need to determine the radius of the outermost circle. You will start drawing a circle with this radius and then draw circles with a decreasing radius for as long as the radius remains positive.

For the maximum radius, you are going to use half of the hypotenuse of the entire view. This means that the outermost circle will nearly circumscribe the view, and you will only see bits of light gray in the corners.

In BNRHypnosisView.m, replace the code that draws one circle with code that draws concentric circles.

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// Figure out the center of the bounds rectangle

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

// The circle will be the largest that will fit in the view

float radius = (MIN(bounds.size.width, bounds.size.height) / 2.0);

// The largest circle will circumscribe the view

float maxRadius = hypot(bounds.size.width, bounds.size.height) / 2.0;

UIBezierPath *path = [[UIBezierPath alloc] init];

// Add an arc to the path at center, with radius of radius,

// from 0 to 2*PI radians (a circle)

[path addArcWithCenter:center

radius:radius

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) {

[path addArcWithCenter:center

radius:currentRadius // Note this is currentRadius!

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

}

// Configure line width to 10 points

path.lineWidth = 10.0;

// Draw the line!

[path stroke];

}

Build and run the application. It is not quite what you were expecting; it looks more like crop circles than concentric circles (Figure 4.19).

Figure 4.19 BNRHypnosisView drawing crop circles

BNRHypnosisView drawing crop circles

The problem is that your single UIBezierPath object is connecting the sub-paths (the individual circles) to form the complete path. Think of a UIBezierPath object as a pencil on a piece of paper – when you go to draw another circle, the pencil stays on the piece of paper. You need to lift the pencil off the piece of paper before drawing a new circle.

In the for loop in BNRHypnosisView’s drawRect:, pick up the pencil and move it to the correct spot before drawing each circle.

- (void)drawRect:(CGRect)rect

{

CGRect bounds = self.bounds;

// Figure out the center of the bounds rectangle

CGPoint center;

center.x = bounds.origin.x + bounds.size.width / 2.0;

center.y = bounds.origin.y + bounds.size.height / 2.0;

// The largest circle will circumscribe the view

float maxRadius = hypot(bounds.size.width, bounds.size.height) / 2.0;

UIBezierPath *path = [[UIBezierPath alloc] init];

for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) {

[path moveToPoint:CGPointMake(center.x + currentRadius, center.y)];

[path addArcWithCenter:center

radius:currentRadius // note this is currentRadius!

startAngle:0.0

endAngle:M_PI * 2.0

clockwise:YES];

}

// Configure line width to 10 points

path.lineWidth = 10.0;

// Draw the line!

[path stroke];

}

Build and run the application. You should now have concentric circles.

Figure 4.20 BNRHypnosisView drawing concentric circles

BNRHypnosisView drawing concentric circles

You have seen only a sampling of what UIBezierPath can do. Be sure to check out the documentation and try some of the challenges at the end of this chapter to get a better feel for some of the clever things you can do by stringing together arcs, lines, and curves.

More Developer Documentation

The API Reference, which contains the class references, is an essential part of the developer documentation and an essential part of a developer’s life. But there is more to the documentation than the API Reference. The documentation also provides:

SDK Guides

organized by topic rather than by class or method and excellent for learning more about specific topics in Objective-C and iOS development

Sample Code

small, complete projects that demonstrate how Apple expects the class in question to be used

It would be difficult to overstate how important Apple’s documentation is to the daily work of iOS developers. As you go through this book, take a moment to look up new classes and methods as you encounter them and see what else they can do. Also read through SDK guides and download sample code projects that pique your interest. You can see the available guides and sample code in the iOS Developer Library at developer.apple.com/library.

Bronze Challenge: Draw an Image

The challenge is to load an image from the filesystem and draw it on top of the concentric circles, as in Figure 4.21.

Figure 4.21 Drawing an Image

Drawing an Image

Find an image file. A PNG with some transparent parts would be especially interesting. (The zip file you downloaded has logo.png that will work nicely.) Drag it into your Xcode project.

Creating a UIImage object from that file is one line:

UIImage *logoImage = [UIImage imageNamed:@"logo.png"];

In your drawRect: method, compositing it onto your view is just one more:

[logoImage drawInRect:someRect];

For the More Curious: Core Graphics

In general, the drawRect: method uses UIImage, UIBezierPath, and NSString instances to draw images, shapes, and text, respectively. Each of these classes implements at least one method that, when executed in drawRect:, draws pixels to the layer of the view that was sent drawRect:.

These classes make iOS drawing look simple and convenient. However, there is a lot going on underneath the hood.

Drawing images in iOS – whether it is an image you will save as a JPEG or PDF or a layer that represents a UIView – is the responsibility of the Core Graphics framework. The classes that you used to perform drawing in this chapter, like UIBezierPath, wrap Core Graphics code into their methods to ease drawing for the programmer. To truly understand how theses classes work and how images are created, you should understand how Core Graphics does its job.

Core Graphics is a 2D drawing API written in C. As such, there are no Objective-C objects or methods, but instead C structures and C functions that mimic object-oriented behavior. The most important Core Graphics “object” is the graphics context, which really holds two things: the state of drawing, like the current color of the pen and its line thickness, and the memory that is being drawn upon. A graphics context is represented by “instances” of CGContextRef.

Right before drawRect: is sent to an instance of UIView, the system creates a CGContextRef for that view’s layer. The layer has the same bounds as the view and some default values for its drawing state. As drawing operations are sent to the context, the pixels in the layer are changed. AfterdrawRect: completes, the system grabs the layer and composites it to the screen.

The drawing classes you used in this chapter all know how to call Core Graphics functions that change the drawing state and issue drawing operations on the appropriate CGContextRef. For example, sending setStroke to an instance of UIColor will call functions that change the drawing state of the current context. So, these two chunks of code are equivalent:

[[UIColor colorWithRed:1.0 green:0.0 blue:1.0 alpha:1.0] setStroke];

UIBezierPath *path = [UIBezierPath bezierPath];

[path moveToPoint:a];

[path addLineToPoint:b];

[path stroke];

Is equivalent to these lines:

CGContextSetRGBStrokeColor(currentContext, 1, 0, 0, 1);

CGMutablePathRef path = CGPathCreateMutable();

CGPathMoveToPoint(path, NULL, a.x, a.y);

CGPathAddLineToPoint(path, NULL, b.x, b.y);

CGContextAddPath(currentContext, path);

CGContextStrokePath(currentContext);

CGPathRelease(path);

The Core Graphics functions that operate on the context, like CGContextSetRGBStrokeColor, take a pointer to context that they will modify as their first argument. You can grab a pointer to the current context in drawRect: by calling the function UIGraphicsGetCurrentContext. The current context is an application-wide pointer that is set to point to the context created for a view right before that view is sent drawRect:.

- (void)drawRect:(CGRect)rect

{

CGContextRef currentContext = UIGraphicsGetCurrentContext();

CGContextSetRGBStrokeColor(currentContext, 1, 0, 0, 1);

CGMutablePathRef path = CGPathCreateMutable();

CGPathMoveToPoint(path, NULL, a.x, a.y);

CGPathAddLineToPoint(path, NULL, b.x, b.y);

CGContextAddPath(currentContext, path);

CGContextStrokePath(currentContext);

CGPathRelease(path);

CGContextSetStrokeColorWithColor(currentContext, color);

}

Anything you can do with UIBezierPath and UIColor can be done directly in Core Graphics. In fact, there are C structures that have the same behavior as these classes (CGMutablePathRef and CGColorRef). However, it is usually easier to work with the Objective-C counterparts.

Also, there are some things you just cannot do yet without dropping down to Core Graphics, like drawing gradients. However, because you remembered that types from frameworks all have the same prefix, you can search the documentation for types beginning in CG to find out what is available to you.

You might be wondering why many of the Core Graphics types have a Ref after them. Every Core Graphics type is a structure, but some mimic the behavior of objects by being allocated on the heap. Therefore, when you create one of these Core Graphics “objects”, you are returned a pointer to their address in memory.

Each Core Graphics structure that is allocated in this way has a type definition that incorporates the asterisk (*) into the type itself. For example, there exists a structure CGColor (that you never use) and a type definition CGColorRef that means CGColor * (that you always use). This convention makes it easy for a programmer to glance at code and determine whether or not the variable is a C structure masquerading as an object or an Objective-C object that you can send messages to.

Another point of confusion for programmers in Core Graphics is that some types do not have a Ref or an asterisk, like CGRect and CGPoint. These types are small data structures that can live on the stack, so there is no need to pass a pointer to them.

However, some Core Graphics types are much more involved than simply holding onto a few floats – they actually have pointers to other Core Graphics objects. These “objects” will take strong ownership of the objects they point to, but ARC will not track this ownership. Instead, you must manually release ownership of these types of objects when you are done with them. The rule is: if you create a Core Graphics object with a function that has the word Create or Copy in it, you must call the matching Release function and pass a pointer to the object as the first argument.

One final note about Core Graphics: It exists on the Mac, too. You can write code, as done in the open source core-plot framework, that will work on both iOS and OS X.

Gold Challenge: Shadows and Gradients

At this time, adding drop shadows and drawing with gradients can only be done using Core Graphics.

To create a drop shadow, you install a shadow on the graphics context. After that, anything opaque that you draw will have a drop shadow. The shadow has an offset (which is expressed with an CGSize), and a blur in points. Here is the declaration of the method used to install the shadow on the graphics context:

void CGContextSetShadow (

CGContextRef context,

CGSize offset,

CGFloat blur);

(There is a version that takes a color, but you almost always want a dark shadow.)

There is no unset shadow function. Thus, you will need to save the graphics state before setting the shadow and then restore it after setting the shadow. It looks something like this:

CGContextSaveGState(currentContext);

CGContextSetShadow(currentContext, CGSizeMake(4,7), 3);

// Draw stuff here, it will appear with a shadow

CGContextRestoreGState(currentContext);

// Draw stuff here, it will appear with no shadow

The first part of the challenge is to put a drop shadow on the image you composited onto the view in the previous challenge.

Figure 4.22 Drop Shadow

Drop Shadow

Gradients allow you to do shading that moves smoothly through a list of colors. The CGGradientRef has a list of colors and you ask it to draw the list either linear or radial. It looks like this:

CGFloat locations[2] = { 0.0, 1.0 };

CGFloat components[8] = { 1.0, 0.0, 0.0, 1.0, // Start color is red

1.0, 1.0, 0.0, 1.0 }; // End color is yellow

CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();

CGGradientRef gradient = CGGradientCreateWithColorComponents(colorspace, components,

locations, 2);

CGPoint startPoint = ...;

CGPoint endPoint = ...;

CGContextDrawLinearGradient(currentContext, gradient, startPoint, endPoint, 0);

CGGradientRelease(gradient);

CGColorSpaceRelease(colorspace);

The last argument to CGContextDrawLinearGradient() determines what happens before the start point and after the end point. If you want the first color to cover the space before the start point, you supply kCGGradientDrawsBeforeStartLocation. If you want the last color to cover the space after the end point, you supply kCGGradientDrawsAfterEndLocation. To use both, bitwise or them together:

CGContextDrawLinearGradient(currentContext, gradient, startPoint, endPoint,

kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);

The tricky thing about gradients is that they cover everything in the view. Before drawing a gradient, you typically install a clipping path on the graphics context that defines what you want painted in the gradient. Then, you draw the gradient. Once again, there is no function for clearing the clip path, so you typically save the graphics state before installing the clipping path and restore the state afterward.

If you have a CGContextRef and a UIBezierPath, here is how you install that path as the clipping path:

CGContextSaveGState(currentContext);

[myPath addClip];

// Draw your gradient here

CGContextRestoreGState(currentContext);

The challenge is to fill a triangle with a gradient that goes from yellow at the bottom to green at the top.

Figure 4.23 Gradient Triangle

Gradient Triangle