iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (27 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
5.14Mb size Format: txt, pdf, ePub
Core Graphics

The functions and types that begin with
CG
come from the Core Graphics framework, a C language API for 2D drawing. The hub of the Core Graphics framework is
CGContextRef
: all other Core Graphics functions and types interact with a drawing context in some way, and then the context creates an image.

 

Let’s look at the Core Graphics functions you used in the implementation of
HypnosisView
’s
drawRect:
method. You set up the drawing state with
CGContextSetLineWidth
and then set its color using
CGContextSetRGBStrokeColor
.

 

Next, you added a
path
to the context using
CGContextAddArc
. A path is a collection of points that forms a shape and can form anything from a square to a circle to a human outline. There are a number of Core Graphics functions like
CGContextAddArc
that you can use to add a path to a context. (Search the documentation for
CGContextRef
to find them.)

 

After a path has been added to a context, you can perform a drawing operation. There are three drawing operations:

 
  • CGContextStrokePath
    will draw a line along the path.
 
  • CGContextFillPath
    will fill the shape made by the path.
 
  • CGContextClip
    will limit future drawing operations to the area defined by the path. For example, drawing a square to a context that has been clipped to a circle of the same size will result in drawing a circle.
 

After a drawing operation, the current path is removed from the context. Thus, to draw more than one circle, you have to add a path to the context for each circle. In
HypnosisView.m
, modify
drawRect:
to draw a number of concentric circles.

 
- (void)drawRect:(CGRect)dirtyRect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGRect bounds = [self bounds];
    CGPoint center;
    center.x = bounds.origin.x + bounds.size.width / 2.0;
    center.y = bounds.origin.y + bounds.size.height / 2.0;
    
float maxRadius = hypot(bounds.size.width, bounds.size.height) / 4.0;
    
float maxRadius = hypot(bounds.size.width, bounds.size.height) / 2.0;
    CGContextSetLineWidth(ctx, 10);
    CGContextSetRGBStrokeColor(ctx, 0.6, 0.6, 0.6, 1.0);
    
CGContextAddArc(ctx, center.x, center.y, maxRadius, 0.0, M_PI * 2.0, YES);
    
CGContextStrokePath(ctx);
    // Draw concentric circles from the outside in
    for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) {
        // Add a path to the context
        CGContextAddArc(ctx, center.x, center.y,
                        currentRadius, 0.0, M_PI * 2.0, YES);
        // Perform drawing instruction; removes path
        CGContextStrokePath(ctx);
    }
}

Build and run the application again. This time, each
HypnosisView
draws a number of circles.

 

That is one ugly screen; let’s get rid of one of the instances of
HypnosisView
and make the remaining one the size of the screen. In
HypnosisterAppDelegate.m
, modify
application:didFinishLaunchingWithOptions:
.

 
CGRect viewFrame = CGRectMake(160, 240, 100, 150);
HypnosisView *view = [[HypnosisView alloc] initWithFrame:viewFrame];
HypnosisView *view = [[HypnosisView alloc] initWithFrame:[[self window] bounds]];
[[self window] addSubview:view];
CGRect anotherFrame = CGRectMake(20, 30, 50, 50);
HypnosisView *anotherView = [[HypnosisView alloc] initWithFrame:anotherFrame];
[view addSubview:anotherView];

Build and run the application. Now you have a single instance of
HypnosisView
that fills the entire screen. Much better.

 

There are Core Graphics functions to draw most anything you like. Check the documentation for functions and types beginning with
CG
.

 
UIKit Drawing Additions

There are a number of Foundation and UIKit classes that can work with a
CGContextRef
. One such class is
UIColor
. An instance of
UIColor
represents a color and can be used to set the current drawing color of the context. In
HypnosisView.m
, replace the line of code that changes the stroke color with one that uses
UIColor
’s
colorWithRed:green:blue:alpha:
method.

 
CGContextSetLineWidth(ctx, 10);
CGContextSetRGBStrokeColor(ctx, 0.6, 0.6, 0.6, 1.0);
[[UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1] setStroke];

Build and run the application again. The
HypnosisView
will look the same as before.

 

There are a number of prepared
UIColor
instances you can use for common colors. Change this line of code again:

 
[[UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1] setStroke];
[[UIColor lightGrayColor] setStroke];

Build and run again. The color of the circles should be about the same, but the code is much simpler.

 

NSString
has the ability to draw to a
CGContextRef
. Sending the message
drawInRect:withFont:
to an
NSString
will draw that string in the given rectangle with the given font to the current context. In
HypnosisView.m
, add the following code to the end of
drawRect:
.

 
    for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) {
        CGContextAddArc(ctx, center.x, center.y,
                        currentRadius, 0, M_PI * 2.0, YES);
        CGContextStrokePath(ctx);
    }
    // Create a string
    NSString *text = @"You are getting sleepy.";
    // Get a font to draw it in
    UIFont *font = [UIFont boldSystemFontOfSize:28];
    CGRect textRect;
    // How big is this string when drawn in this font?
    textRect.size = [text sizeWithFont:font];
    // Let's put that string in the center of the view
    textRect.origin.x = center.x - textRect.size.width / 2.0;
    textRect.origin.y = center.y - textRect.size.height / 2.0;
    // Set the fill color of the current context to black
    [[UIColor blackColor] setFill];
    // Draw the string
    [text drawInRect:textRect
            withFont:font];
}
 

Let’s spice it up with a shadow. Add the following code in
HypnosisView.m
.

 
[[UIColor blackColor] setFill];
// The shadow will move 4 points to the right and 3 points down from the text
CGSize offset = CGSizeMake(4, 3);
// The shadow will be dark gray in color
CGColorRef color = [[UIColor darkGrayColor] CGColor];
// Set the shadow of the context with these parameters,
// all subsequent drawing will be shadowed
CGContextSetShadowWithColor(ctx, offset, 2.0, color);
// Draw the string
[text drawInRect:textRect
        withFont:font];

Build and run the application. You now have shadow.

 

UIColor
’s
setStroke
method replaces
CGContextSetRGBStrokeColor
, but it is a one-to-one line replacement, so it doesn’t save that much work.
NSString
’s
drawInRect:withFont:
, on the other hand, buys you a lot of convenience.
UIImage
has a similar useful method,
drawInRect:
, for drawing an image object to a context.

 
Redrawing Views

When a
UIView
instance receives the message
setNeedsDisplay
, it will redraw its image. View subclasses send themselves
setNeedsDisplay
when their drawable content changes. For example, a
UILabel
will mark itself for re-display if it is sent the message
setText:
. (It has to redraw its image if the text it displays changes.)

 

The redrawing of a view’s image is not immediate; instead, it is added to a list of views that need updating. To understand when views actually get redrawn, we need to talk about the
run loop
– the infinite loop that comprises an iOS application. The run loop’s job is to listen for input (a touch, Core Location updates, data coming in through a network interface, etc.) and then find the appropriate handlers for that event (like an action or a delegate method for an object). Those handler methods call other methods, which call more methods, and so on. Once all the methods have completed, control returns to the run loop. At this point, the views are redrawn.
Figure 6.10
shows where redrawing views happens in the run loop.

 

Figure 6.10  Redrawing views with the run loop

 

When control returns to the run loop, it says,

Well, a bunch of code was just executed. I’m going to check if any views need to be redrawn.

Then the run loop prepares the necessary drawing contexts and sends the message
drawRect:
to all of the views that have been sent
setNeedsDisplay
in this iteration of the loop.

 

If one view redraws its image, the other views on the screen are not required to redraw theirs. Instead, their existing images will just be composited on the screen again. This optimization is why drawing and animation on iOS feels so responsive.

 

To see the effect of redrawing a view’s image, let’s give
HypnosisView
a
circleColor
property. Each circle that the
HypnosisView
draws will be the color pointed to by this property. In
HypnosisView.h
, declare this property.

 
@interface HypnosisView : UIView
{
}
@property (nonatomic, strong) UIColor *circleColor;
@end
 

In
HypnosisView.m
, synthesize this property to automatically create the instance variable, setter, and getter.

 
@implementation HypnosisView
@synthesize circleColor;
 

Now, in
HypnosisView.m
, update the
initWithFrame:
method to create a default
circleColor
.

 
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setBackgroundColor:[UIColor clearColor]];
        
[self setCircleColor:[UIColor lightGrayColor]];
    }
    return self;
}
 

Then, in
drawRect:
, modify the message that sets the context’s stroke color to use the
circleColor
instead of light gray.

 
CGContextSetLineWidth(ctx, 10);
[[self circleColor] setStroke];
for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) {
    CGContextAddArc(ctx, center.x, center.y, currentRadius, 0.0, M_PI * 2.0, YES);
    CGContextStrokePath(ctx);
}
 

Build and run the application. The circles are the same color because we don’t have a way to change
circleColor
from the default once the application is running. Let’s make the circle color change when the user shakes the device. To find out how to respond to a shake, we need to see how motion events are handled.

 

Other books

A Spanish Engagement by Kathryn Ross
After the Fall by Morgan O'Neill
A Wild Light by Marjorie M. Liu
Alice in Bed by Judith Hooper
Fierce Passion by Phoebe Conn