显示标签为“Draw”的博文。显示所有博文
显示标签为“Draw”的博文。显示所有博文

2012年1月6日星期五

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 2

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 2:
This is a blog post by iOS Tutorial Team member Tope Abayomi, an iOS developer and Founder of App Design Vault, your source for iPhone App Design.

Learn how to create a PDF programmatically!
Learn how to create a PDF programmatically!

Welcome to the second part of the tutorial series on how to generate a PDF using Quartz 2D!

In part one, we set up an app framework for the PDF, and we drew some rudimentary text and lines with Quartz 2D inside our PDF.

Since our PDF will be an invoice document, it needs to look professional. To accomplish this, here in Part Two we’ll be adding a logo image and drawing a table to hold the invoice data. By the end of this article, our PDF will be complete.

Let’s jump back in!



Adding An Image


The image we’re going to draw to our PDF is the logo of none other than RayWenderlich.com. All part of Ray’s scheme to take over the world! ;]

Go ahead and download the image, then add it to the project. To do this, control-click on the PDFRenderer group in the Project navigator and choose “Add Files To iOSPDFRenderer.” Select the ‘ray-logo.png’ image you downloaded and then click Add.



Next open PDFRenderer.m and add this method:


+(void)drawImage:(UIImage*)image inRect:(CGRect)rect
{

[image drawInRect:rect];

}


Yes, drawing an image with Core Graphics is that easy! All we need is this one-liner to draw an image to the current context.

The method takes in the image we want to draw and the frame where it will be drawn, and draws the image into the context.

Next add the definition of this method in PDFRenderer.h:


+(void)drawImage:(UIImage*)image inRect:(CGRect)rect;


Now we need to call the method so that it’s displayed on the PDF. Add the following lines of code to the drawPDF method on the PDFRenderer.m file (right before the call to UIGraphicsEndPDFContext):


UIImage* logo = [UIImage imageNamed:@"ray-logo.png"];
CGRect frame = CGRectMake(20, 100, 300, 60);
[PDFRenderer drawImage:logo inRect:frame];


In the code above, we create a UIImage from the image file, define the location and size of the image to be drawn, then call the drawImage method we created above with these two parameters.

Here’s the complete version of the drawPDF method:


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

[self drawText];

CGPoint from = CGPointMake(0, 0);
CGPoint to = CGPointMake(200, 300);
[PDFRenderer drawLineFromPoint:from toPoint:to];

UIImage* logo = [UIImage imageNamed:@"ray-logo.png"];
CGRect frame = CGRectMake(20, 100, 300, 60);

[PDFRenderer drawImage:logo inRect:frame];

// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
}


Now if you run the application in the simulator, you’ll see our PDF as we left it in Part One, only now with Ray’s logo. Of course, it still looks pretty crappy. Time to fix that!



Drawing the Labels


At this point, we know how to draw the basic elements of the invoice: text, lines and images. The next step is to combine all the elements to create a polished layout.

To do this, we’ll use a little trick. We’ll create a View in the interface builder and add the elements for the invoice. Then we’ll use the locations of the views to make the layout.

This will make things a lot easier because we can visually layout the PDF, and won’t have to hard-code as many coordinates for drawing. Don’t worry, it will begin to make sense as we do it!

Create a new View in the application by choosing File\New\New File. Select the iOS\User Interface\View template and click on Next. Make sure the Device family is set to iPhone and click Next.

Give the new View the name InvoiceView and click Create. Select the View on the Interface Builder and delete it by hitting the backspace button.



Add a new View from the Objects tab onto the canvas. Resize the view to be 612 wide and 792 tall. These are the default dimensions for an A4 PDF file.



Add eight labels to the View and give them the following names:

  • Recipient [Name]
  • Recipient’s Address
  • Recipient’s City
  • Recipient’s Postal Code
  • Invoicer [Name]
  • Invoicer’s Address
  • Invoicer’s City
  • Invoicer’s Postal Code



The positions of these labels will form our layout for the invoice. Give each label a tag from 0-7. For example, the label “Recipient” will have a tag of 0, “Recipient’s Address” will have a tag of 1, and so on.



We’re done with the layout view for now. We’ll come back to it in a bit.

But first we need to open PDFRenderer.m and refactor the drawText method. We want to pass in the text to be drawn and the frame that will encompass it, rather than hard-coding it in.

Go ahead and replace drawText with the following (pretty much the same except pulling out the hardcoded string and frame):


+(void)drawText:(NSString*)textToDraw inFrame:(CGRect)frameRect
{
CFStringRef stringRef = (__bridge CFStringRef)textToDraw;
// Prepare the text using a Core Text Framesetter.
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, stringRef, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);

CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);

// Get the frame that will do the rendering.
CFRange currentRange = CFRangeMake(0, 0);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);

// Get the graphics context.
CGContextRef    currentContext = UIGraphicsGetCurrentContext();

// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);

// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 100);
CGContextScaleCTM(currentContext, 1.0, -1.0);

// Draw the frame.
CTFrameDraw(frameRef, currentContext);

CFRelease(frameRef);
CFRelease(stringRef);
CFRelease(framesetter);
}


Then add the new definition to PDFRenderer.h:


+(void)drawText:(NSString*)textToDraw inFrame:(CGRect)frameRect;


The next step is to load the labels from the InvoiceView and use the text and the frame to draw to the PDF. Add this new method to PDFRenderer.m, right above drawPDF:


+(void)drawLabels
{   
NSArray* objects = [[NSBundle mainBundle] loadNibNamed:@"InvoiceView" owner:nil options:nil];

UIView* mainView = [objects objectAtIndex:0];

for (UIView* view in [mainView subviews]) {
if([view isKindOfClass:[UILabel class]])
{
UILabel* label = (UILabel*)view;

[self drawText:label.text inFrame:label.frame];
}
}
}


This will load the labels from the InvoiceView, loop through all the labels and call the drawText method with its text and frame variables.

Next we’ll modify the drawPDF method call this method and remove all the test drawing code we used previously. Replace drawPDF with the following:


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

[self drawText:@"Hello World" inFrame:CGRectMake(0, 0, 300, 50)];

[self drawLabels];

// Close the PDF context and write out the contents.
UIGraphicsEndPDFContext();
}


Let’s try it out! Run the application in the simulator, and you will see something similar to the image below.



Yikes – it looks like garbage, right? That’s because the text is being flipped and translated multiple times in the the drawText method. Up until now, we’ve only drawn one line of text, but the problem arises when we want to draw multiple lines of text.

To fix this, we need to modify the drawText method to flip the current context back to its original coordinates. Change the following lines in the drawText method to add the reverse-flip actions and take the origin into consideration:


// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
// Modify this to take into consideration the origin.
CGContextTranslateCTM(currentContext, 0, frameRect.origin.y*2);
CGContextScaleCTM(currentContext, 1.0, -1.0);

// Draw the frame.
CTFrameDraw(frameRef, currentContext);


// Add these two lines to reverse the earlier transformation.
CGContextScaleCTM(currentContext, 1.0, -1.0);
CGContextTranslateCTM(currentContext, 0, (-1)*frameRect.origin.y*2);


Now run the application again. You should see a nicely laid-out PDF with all our labels mapped.



Adding the Logo


Next, open InvoiceView.xib and add a UIImageView to the upper right for the logo:



Then add this new method into PDFRenderer.m (right before drawPDF):


+(void)drawLogo
{   
NSArray* objects = [[NSBundle mainBundle] loadNibNamed:@"InvoiceView" owner:nil options:nil];

UIView* mainView = [objects objectAtIndex:0];

for (UIView* view in [mainView subviews]) {
if([view isKindOfClass:[UIImageView class]])
{           
UIImage* logo = [UIImage imageNamed:@"ray-logo.png"];
[self drawImage:logo inRect:view.frame];
}
}   
}


This loads the UIImageView from the Xib file in the same way we load the labels. Then it draws the logo onto the PDF using the coordinates of the UIImageView.

Finally, call this method after drawLabels in the drawPDF method:


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

[self drawText:@"Hello World" inFrame:CGRectMake(0, 0, 300, 50)];

[self drawLabels];
[self drawLogo];

// Close the PDF context and write out the contents.
UIGraphicsEndPDFContext();
}


Run the application in the simulator, and you’ll see the logo in the top right corner just as we specified!



Pretty cool how easy it is to visually lay out the PDF with the aid of UIView coordinates, eh?

Drawing the Table


It’s time to add the table that will hold all the invoice information. Our table will be nothing more than an arrangement of vertical and horizontal lines.

In this case, we won’t use the InvoiceView. Instead we’ll use a series of variables like the table height and width, and the row height and column width.

Add the following method to PDFRenderer.m (right before drawPDF):


+(void)drawTableAt:(CGPoint)origin
withRowHeight:(int)rowHeight
andColumnWidth:(int)columnWidth
andRowCount:(int)numberOfRows
andColumnCount:(int)numberOfColumns

{  
for (int i = 0; i <= numberOfRows; i++)
{       
int newOrigin = origin.y + (rowHeight*i);

CGPoint from = CGPointMake(origin.x, newOrigin);
CGPoint to = CGPointMake(origin.x + (numberOfColumns*columnWidth), newOrigin);

[self drawLineFromPoint:from toPoint:to];
}
}


This method draws our horizontal lines. At the beginning of the method, we pass in the values for the starting position of the table, the number of rows and columns, the height of each row and the width of each column.

The loop then goes through the motions for each row, calculating where the row should start and where it should end. Finally, the drawLine:from:to method is called to draw the line. We then add the following code to the drawPDF method:


+(void)drawPDF:(NSString*)fileName
{
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);

[self drawText:@"Hello World" inFrame:CGRectMake(0, 0, 300, 50)];

[self drawLabels];
[self drawLogo];

int xOrigin = 50;
int yOrigin = 300;

int rowHeight = 50;
int columnWidth = 120;

int numberOfRows = 7;
int numberOfColumns = 4;

[self drawTableAt:CGPointMake(xOrigin, yOrigin) withRowHeight:rowHeight andColumnWidth:columnWidth andRowCount:numberOfRows andColumnCount:numberOfColumns];

// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
}


Run the application in the simulator and you should see the horizontal lines on the PDF.



The next step is to draw the vertical lines. Add another loop to the drawTable method below the first loop:


+(void)drawTableAt:(CGPoint)origin
withRowHeight:(int)rowHeight
andColumnWidth:(int)columnWidth
andRowCount:(int)numberOfRows
andColumnCount:(int)numberOfColumns

{  
for (int i = 0; i <= numberOfRows; i++)
{       
int newOrigin = origin.y + (rowHeight*i);

CGPoint from = CGPointMake(origin.x, newOrigin);
CGPoint to = CGPointMake(origin.x + (numberOfColumns*columnWidth), newOrigin);

[self drawLineFromPoint:from toPoint:to];       
}

for (int i = 0; i <= numberOfColumns; i++)
{       
int newOrigin = origin.x + (columnWidth*i);

CGPoint from = CGPointMake(newOrigin, origin.y);
CGPoint to = CGPointMake(newOrigin, origin.y +(numberOfRows*rowHeight));

[self drawLineFromPoint:from toPoint:to];       
}
}


The second loop in the above code does a similar run through all the columns in the table, calculating the start and end points of each line and drawing the line to the PDF.

If you run the application again, you will see that our table is complete!



But what use is a table without data?

Populating the Table


We’re going to manually add some dummy data to our table using a series of arrays. But you could easily modify this to feed in data inputed by the user.

Add the following method called drawTableDataAt to PDFRenderer.m (right above drawPDF):


+(void)drawTableDataAt:(CGPoint)origin
withRowHeight:(int)rowHeight
andColumnWidth:(int)columnWidth
andRowCount:(int)numberOfRows
andColumnCount:(int)numberOfColumns
{ 
NSArray* headers = [NSArray arrayWithObjects:@"Quantity", @"Description", @"Unit price", @"Total", nil];
NSArray* invoiceInfo1 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo2 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo3 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo4 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];

NSArray* allInfo = [NSArray arrayWithObjects:headers, invoiceInfo1, invoiceInfo2, invoiceInfo3, invoiceInfo4, nil];

for(int i = 0; i < [allInfo count]; i++)
{
NSArray* infoToDraw = [allInfo objectAtIndex:i];

for (int j = 0; j < numberOfColumns; j++)
{

int newOriginX = origin.x + (j*columnWidth);
int newOriginY = origin.y + ((i+1)*rowHeight);

CGRect frame = CGRectMake(newOriginX, newOriginY, columnWidth, rowHeight);

[self drawText:[infoToDraw objectAtIndex:j] inFrame:frame];
}       
}   
}


This code block begins by creating the data that will be in the table. The first array contains the values that will be in the header (first row of the table). The next three arrays contain the values for each row and column of the table.

The final array is a master array containing all the others, effectively making it a two-dimensional array modeling our table data.

After that, there are two nested loops. The outer loop runs through each row and extracts the data for the row.

The inner loop runs through each column and calculates the starting point of the text, depending on its location in the table. It creates a frame for the text and then draws the text in the frame.

Add a call to this new method in the drawPDF method (right before the call to UIGraphicsEndPDFContext):


[self drawTableDataAt:CGPointMake(xOrigin, yOrigin) withRowHeight:rowHeight andColumnWidth:columnWidth andRowCount:numberOfRows andColumnCount:numberOfColumns];


Run the application in the simulator and you will see our table is populated with data.



Looks good, doesn’t it? But let’s make one final tweak: we could use some padding between the table lines and the data itself.

Below is the final state of the drawTableAt method:


+(void)drawTableDataAt:(CGPoint)origin
withRowHeight:(int)rowHeight
andColumnWidth:(int)columnWidth
andRowCount:(int)numberOfRows
andColumnCount:(int)numberOfColumns
{
int padding = 10;

NSArray* headers = [NSArray arrayWithObjects:@"Quantity", @"Description", @"Unit price", @"Total", nil];
NSArray* invoiceInfo1 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo2 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo3 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];
NSArray* invoiceInfo4 = [NSArray arrayWithObjects:@"1", @"Development", @"$1000", @"$1000", nil];

NSArray* allInfo = [NSArray arrayWithObjects:headers, invoiceInfo1, invoiceInfo2, invoiceInfo3, invoiceInfo4, nil];

for(int i = 0; i < [allInfo count]; i++)
{
NSArray* infoToDraw = [allInfo objectAtIndex:i];

for (int j = 0; j < numberOfColumns; j++)
{           
int newOriginX = origin.x + (j*columnWidth);
int newOriginY = origin.y + ((i+1)*rowHeight);

CGRect frame = CGRectMake(newOriginX + padding, newOriginY + padding, columnWidth, rowHeight);

[self drawText:[infoToDraw objectAtIndex:j] inFrame:frame];
}       
}   
}


Now we can sit back and admire the final invoice PDF, displaying image, table and data. Our work is done… that is, until it’s time for all of us to start billing for RayWenderlich.com. :P



Where to Go From Here?


Here is an example project with all of the code from the above tutorial.

That’s all folks! This tutorial series should have given you an idea of how to use Quartz 2D to generate a PDF displaying results from your app. From here, there are many implementation possibilities!

As I said at the beginning of this tutorial series, going through this project should have also helped you appreciate all of the low-level layout Apple gives us for free in the UIKit.

I look forward to reading your questions and comments in the forum discussion below!



This is a blog post by iOS Tutorial Team member Tope Abayomi, an iOS developer with a passion for easy to use, useful, aesthetically pleasing apps. Here are some of his videos teaching you how to design an app with custom design.

How To Create a PDF with Quartz 2D in iOS 5 Tutorial Part 2 is a post from: Ray Wenderlich

2011年12月29日星期四

Using blocks for drawing to avoid subclassing in Objective-C

Using blocks for drawing to avoid subclassing in Objective-C

It is very common for a designer to ask for a 1 pixel tall bar here, or a small gradient there. This is a small request that isn’t very hard, right? Subclass UIView, override drawRect and do the drawing using Core Graphics. But every time you do this you need to add a file to your project. And all this file does is include 1 drawRect method with likely very little code. It personally bothers me when I see lot’s of these little classes that don’t do very much.

Block Based Solution

Instead of subclassing UIView every time we need to draw something what if we were to have one subclass that allowed us to pass in a block that performed the drawing code. So I’ve created a class called DrawView that does exactly that. It also passes itself and the graphics context since that was going to be needed in every block’s implementation so including them as parameters reduced the amount of boiler plate code needed.
typedef void(^DrawView_DrawBlock)(UIView* v,CGContextRef context);
@interface DrawView : UIView
@property (nonatomic,copy) DrawableView_DrawBlock drawBlock;
@end
#import "DrawView.h"
@implementation DrawView
@synthesize drawBlock;
- (void)dealloc
{
    [drawBlock release], drawBlock = nil;
    [super dealloc];
}
- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    if(self.drawBlock)
        self.drawBlock(self,context);
}
@end
Using this class in action is easy. Simply instantiate a DrawView object and pass in a drawBlock with some Core Graphics code in there.
DrawView* drawableView = [[[DrawView alloc] initWithFrame:CGRectMake(0,0,320,50)] autorelease];
drawableView.drawBlock = ^(UIView* v,CGContextRef context)
{
    CGPoint startPoint = CGPointMake(0,v.bounds.size.height-1);
    CGPoint endPoint = CGPointMake(v.bounds.size.width,v.bounds.size.height-1);
    CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
    CGContextSetLineWidth(context, 1);
    CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
    CGContextAddLineToPoint(context, endPoint.x + 0.5, endPoint.y + 0.5);
    CGContextStrokePath(context);
};
[self.view addSubview:drawableView];

About.

Hi, I'm David Hamrick. I'm currently working as an iOS developer at Mercury in Nashville, TN. I'm also the author of VueScan Mobile, an easy to use app that allows you to scan from HP, Canon, and Epson printer/scanners to your iPhone, iPad, and iPod Touch.

2011年11月3日星期四

Creating a Graph With Quartz 2D: Part 5

Creating a Graph With Quartz 2D: Part 5:
In this series of articles I am discussing the creation of charts and graphs using nothing more than Quartz 2D, a graphics rendering API created by Apple, which is a part of Core Graphics. You might wish to get up to speed with Part 1, Part 2, Part 3 and Part 4.

Drawing Text

Finally, we are going to write some text on the graph. You might be wondering why I delayed such a simple task for so long. Indeed, it doesn’t take a lot of effort to draw a string in Quartz 2D. Let’s try it: in GraphView, in the end of drawRect add the following snippet (it doesn’t matter which kind of graph you’ll be displaying at this stage):
// Drawing text
CGContextSelectFont(context, "Helvetica", 44, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetFillColorWithColor(context, [[UIColor colorWithRed:0 green:0 blue:0 alpha:1.0] CGColor]);
NSString *theText = @"Hi there!";
CGContextShowTextAtPoint(context, 100, 100, [theText cStringUsingEncoding:NSUTF8StringEncoding], [theText length]);
Nothing complex here: we are selecting a font, a color, a drawing mode and then drawing a random string somewhere inside the graph. How do you think the result will look? Let’s see:
Quartz 2D Part V Figure 1
Figure 1
Oops! Not exactly what we wanted: the text is drawn upside down. That’s because in the system of coordinates used in iOS the vertical coordinate goes from top to bottom. Basically, everything is drawn upside down. You might have noticed this already if you were trying to use a more or less complex graphical background.
We can keep this peculiarity in mind when drawing the graph, and adjust the logic accordingly. We could use a different background after all, but when it comes to drawing text, we have to face the problem.
Fortunately, the solution isn’t difficult. All we need to do is transform the system of coordinates in such a way that the vertical axis starts at the bottom of the graph and goes upwards – and there are a few functions that allow us to do exactly that. In fact, we can resolve our problem with a single line of code. Insert it before any text drawing is done:
CGContextSetTextMatrix (context, CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 0.0));
Run the app, and the text should look all right this time:
Quartz 2D Part V Figure 2
Figure 2
Let’s try and understand the incantation that saved the day. The whole magic is in the six parameters, and I will only mention here those that are non-zero. You can read the detailed explanation of the logic in the documentation for CGAffineTransform.
So we are changing the system of coordinates in use, right? The six parameters describe the relationships between the old system and the new system. The very first parameter describes the relationship between the x coordinate of the new system and the x coordinate of the old system. We don’t want any changes here, so the value of the parameter is 1. The fourth parameter describes the relationship between the y coordinate of the new system and the y coordinate of the old system. We only need to reverse the direction, hence the value is –1.
As you can see, everything is very simple in this particular case. However, you are free to define a more complex transformation, to squeeze and shift the coordinates as you wish. For now, let’s better consider another useful transform. Here is the code:
CGContextSetTextMatrix(context, CGAffineTransformRotate(CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 0.0), M_PI / 2));
Can you guess what it does? Replace the previous magical incantation with this one, et voila:
Quartz 2D Part V Figure 3
Figure 3
Well, I had to change the third parameter of CGContextShowTextAtPoint to 200, to make sure that the text remains within the graph boundaries. See, we can draw text vertically – can be quite useful for drawing labels.
To wrap it up, let’s do something for real, let’s number the data points of the line graph. Here is the code, it shouldn’t surprise you:
CGContextSetTextMatrix(context, CGAffineTransformMake(1.0, 0.0, 0.0, -1.0, 0.0, 0.0));
CGContextSelectFont(context, "Helvetica", 18, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextFill);
CGContextSetFillColorWithColor(context, [[UIColor colorWithRed:0 green:0 blue:0 alpha:1.0] CGColor]);
for (int i = 1; i < sizeof(data); i++)
{
NSString *theText = [NSString stringWithFormat:@"%d", i];
CGContextShowTextAtPoint(context, kOffsetX + i * kStepX, kGraphBottom - 5, [theText cStringUsingEncoding:NSUTF8StringEncoding], [theText length]);
}
And this is the result:
Quartz 2D Part V Figure 4
Figure 4
It looks okay, but it would be much better if the labels were centered under the data points, how do you think? To achieve that, we need to know the width of the label being drawn, and then make a correction to its x coordinate for a half of the label’s width.
Here is the line of code that will give us the size of the label. Obviously, we need to specify the same font that’s being used for drawing text:
CGSize labelSize = [theText sizeWithFont:[UIFont fontWithName:@"Helvetica" size:18]];
Finally, this is the amended line for actually drawing the label:
CGContextShowTextAtPoint(context, kOffsetX + i * kStepX - labelSize.width/2, kGraphBottom - 5, [theText cStringUsingEncoding:NSUTF8StringEncoding], [theText length]);
Run the app again, and now the labels will be nicely aligned under the data points:
Quartz 2D Part V Figure 5
Figure 5
As we said at the beginning of this series, working with Quartz 2D isn’t that hard, you just need to know how to do certain things and once you’ve learnt them, you can move forward quickly. Now you can do everything that is needed to draw a bespoke graph of any complexity using Quartz 2D. The sky is the limit!

Quartz 2D Index

Alexander Kolesnikov’s series on Creating a Graph using Quartz 2D was split into 5 parts. You can refer to the series using the Quartz 2D Tag and access the individual articles using the links below.

Creating a Graph With Quartz 2D: Part 4

Creating a Graph With Quartz 2D: Part 4:
In this series of articles I am discussing the creation of charts and graphs using nothing more than Quartz 2D, a graphics rendering API created by Apple, which is a part of Core Graphics. You might wish to get up to speed with Part 1, Part 2 and Part 3.
Our graphs look great, but there are a couple of things missing. First, it is common to have labels that show the scale of values, to number the data points, to provide some comments and so on. Second, with the fabulous touch screen of the iPhone, the users will probably expect to be able to interact with the graph, for example if they tap it, the graph might respond with an appropriate bit of additional information.
In this part of the series, let’s see how to enable this kind of interactivity. We’ll leave drawing text on the graph for the final part of the series.

Enabling Interactivity

First of all, let’s refactor the existing code somewhat. We are going to switch between bar graph and line graph. It might seem convenient to create two different projects for the two different graphs, and I guess some readers have already done that. For me, it is more convenient to keep all of the code in one project. I will simply move the code for drawing the bar graph into a separate method, placed right above the drawRect:
- (void)drawBarGraphWithContext:(CGContextRef)ctx
{
// Draw the bars
float maxBarHeight = kGraphHeight - kBarTop - kOffsetY;
for (int i = 0; i < sizeof(data); i++)
{
float barX = kOffsetX + kStepX + i * kStepX - kBarWidth / 2;
float barY = kBarTop + maxBarHeight - maxBarHeight * data[i];
float barHeight = maxBarHeight * data[i];
CGRect barRect = CGRectMake(barX, barY, kBarWidth, barHeight);
[self drawBar:barRect context:ctx];
}
}
Now replace in drawRect all the code that went into the new method with a single line. It should be right next to the line that invokes the method for drawing the line graph. By commenting out one of these lines, we’ll be able to switch easily between different types of graph.
[self drawBarGraphWithContext:context];
We are going to deal with the bar graph first. The idea is that whenever a bar is tapped, a message appears indicating the value of that bar, but if the user taps outside of any bar, nothing will happen.

Detecting the Bars Tapped

The approach is quite simple. When drawing bars, we create rectangles and fill them with gradients. If we manage to save those rectangles and keep them around, we should be able to test if the coordinates of a touch happen to be inside of one of them.
Fo simplicity, let’s suppose that we know the number of bars and can make it into a constant. Add the following definition to the already existing ones:
#define kNumberOfBars 12
Add the following line of code to GraphView.m outside of the methods:
CGRect touchAreas[kNumberOfBars];
Also modify the condition of the for loop in drawBarGraphWithContext:
for (int i = 0; i < kNumberOfBars; i++)
Finally, at the very end of this loop, save the rectangle for the bar into the new array:
touchAreas[i] = barRect;
Now, after the drawing of the graph is completed, we’ll have at our disposal an array of rectangles that were used for drawing the bars. The next step is to intercept the user’s touches and define their locations. For this, we need to add the following method:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
NSLog(@"Touch x:%f, y:%f", point.x, point.y);
}
Run the app, tap somewhere inside the graph, and you should see in the log the coordinates of the point that was tapped.
Finally, we need to figure out whether the point that was tapped belongs to any of the bars. Add in the end of the new method the following code snippet:
for (int i = 0; i < kNumberOfBars; i++)
{
if (CGRectContainsPoint(touchAreas[i], point))
{
NSLog(@"Tapped a bar with index %d, value %f", i, data[i]);
break;
}
}
Now if you tap inside one of the bars, you should see logging similar to the following, on the other hand, if you tap somewhere outside of the bars, you should only see the touch message:
MyGraph[1265:b303] Touch x:212.000000, y:126.000000
MyGraph[1265:b303] Tapped a bar with index 2, value 0.900000
Obviously, instead of logging the message that a bar was touched, you can display a view with a label, and set the text of that label to the value of the bar, or do something else that is appropriate for your application. Because that won’t be Quartz 2D specific, I am leaving the detailed implementation to you.
What about the line graph then, how should it react to a touch? One possible solution is to draw a vertical line through a data point that is closest to the location of the touch. I don’t think I really need to show how to do that, you know how to draw lines and you can figure out where exactly to draw the pointer. However, when working on such a pointer line, I’ve found an interesting solution, let me show it to you.

Drawing a Pointer Line

The first approach that I tried was simply adding code for drawing a vertical line to the end of the drawRect method, and running it after the graph was touched. To make the line visible, I had to request a redraw of the whole graph after each touch. This is where I noticed that the graph became a bit sluggish, redrawing the lines, gradients and labels again and again. Clearly, this wasn’t an acceptable solution.
An alternative approach would be to redraw not the whole graph, but only a limited area of it. However, I felt too lazy to do this, and finally found a solution that I believe is simple and nice. The idea is to put another, transparent view on top of the GraphView, let’s call it PointerView, and handle touches and draw the pointer in that view only, leaving the GraphView as it is. Let’s dive into how I did it.
Select MyGraphViewController.xib, then drag a View from the Library and drop it on top of the GraphView. You may need to make some adjustments here. First, make sure that the new View is at the same level of the object hierarchy as the GraphView. To achieve this, drag and drop the new view on top of the Scroll View right in the object tree. Second, set the View’s x and y coordinates to 0. The following screenshot demonstrates what should be the end result of your manipulations.
Quartz 2D Part 4 Figure 1
Figure 1
Next, add to your project a new Objective-C class and make it a subclass of UIView. I named it PointerView. Back to the MyGraphViewController.xib, select the newly added View and change its class to PointerView. You might also want to change its name in the object tree to Pointer View.
While we are here, let’s change the background color of the PointerView to transparent. For this, click on the control for selecting a background color in PointerView’s properties and drag the Opacity slider to the left as far as it can go:
Quartz 2D Part 4 Figure 2
Figure 2
If you run the app now, and don’t forget to switch to line graph mode by commenting/uncommenting the appropriate lines of code, it should work and look exactly as it did before. In PointerView.m, uncomment the drawRect method and add the method for handling touches:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self];
}
Your homework, is the logic for figuring out the closest datapoint and drawing the pointer line through it. Here, we’ll simply draw a vertical line at the point where the graph was touched. Declare a couple of variables in PointerView.h:
#import <UIKit/UIKit.h>
@interface PointerView : UIView
{
float pointerX;
BOOL drawPointer;
}
@end
The first one will store the x coordinate of the latest touch while the second is a flag that will tell it’s all right to draw the pointer line. We can now complete the touchesBegan method by adding to it the following lines:
pointerX = point.x;
drawPointer = YES;
[self setNeedsDisplay];
The last line marks PointerView for redrawing. We now have all the information we need for drawing the pointer, so here is the drawing code:
- (void)drawRect:(CGRect)rect
{
if (drawPointer)
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect frame = self.frame;
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, [[UIColor colorWithRed:0.4 green:0.8 blue:0.4 alpha:1.0] CGColor]);
CGContextMoveToPoint(context, pointerX, 0);
CGContextAddLineToPoint(context, pointerX, frame.size.height);
CGContextStrokePath(context);
}
}
Here is how the graph should look after a touch:
Quartz 2D Part 4 Figure 3
Figure 3
Note that performance won’t suffer, because after a touch we’ll only redraw the pointer line. The only important element that is missing from our graphs now is some textual information. We are going to learn how to draw that text using Quartz 2D in the next, and final part of this series. Stay tuned.

Quartz 2D Index

Alexander Kolesnikov’s series on Creating a Graph using Quartz 2D was split into 5 parts. You can refer to the series using the Quartz 2D Tag and access the individual articles using the links below.

Creating a Graph With Quartz 2D: Part 3

Creating a Graph With Quartz 2D: Part 3:
To create a line graph, I am going to reuse the same project that we used for drawing bars in the previous part of the series. We won’t need the logic for drawing bars anymore, so comment out a dozen of lines of code in the end of the drawRect method, those that deal with bars.
I’ve also changed the value of kOffsetX to 0, so that the line graph starts from the left edge of the view. We’ll need to add a couple of values to the dataset, and you can choose any values between 0 and 1. Here is what I’ve chosen:
float data[] = {0.7, 0.4, 0.9, 1.0, 0.2, 0.85, 0.11, 0.75, 0.53, 0.44, 0.88, 0.77, 0.99, 0.55};
Create a new empty method, making sure that it goes before the drawRect:
- (void)drawLineGraphWithContext:(CGContextRef)ctx
{
}
We’ll be gradually filling this method with code. The first step is to set up the environment, in this case to define the width and the color of the line we are going to use. The following two lines of code shouldn’t surprise you:
CGContextSetLineWidth(ctx, 2.0);
CGContextSetStrokeColorWithColor(ctx, [[UIColor colorWithRed:1.0 green:0.5 blue:0 alpha:1.0] CGColor]);
You are already familiar with drawing paths – we’ve drawn bars as paths – so the following lines of code, although slightly different, should be fully comprehensible:
int maxGraphHeight = kGraphHeight - kOffsetY;
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, kOffsetX, kGraphHeight - maxGraphHeight * data[0]);
for (int i = 1; i < sizeof(data); i++)
{
CGContextAddLineToPoint(ctx, kOffsetX + i * kStepX, kGraphHeight - maxGraphHeight * data[i]);
}
CGContextDrawPath(ctx, kCGPathStroke);
This is all we need to draw a simple line graph. Run the app, and you should see the following:
Quartz 2D Part III Figure 1
Figure 1
For some simple cases this graph might be acceptable, but there are a number of ways in which we can enhance it. One of them is to make the data points more noticeable.

Emphasising Data Points

One popular solution is to draw a little circle on top of each datapoint, so let’s do that. First of all, we’ll want the circles to be filled with color, so add the following line of code to the very end of the drawLineGraphWithContext method:
CGContextSetFillColorWithColor(ctx, [[UIColor colorWithRed:1.0 green:0.5 blue:0 alpha:1.0] CGColor]);
Next, let’s define the circle radius in the header file:
#define kCircleRadius 3
In Quartz 2D, to draw a circle or ellipse, we first need to create a rectangle (CGRect) that will define that circle’s dimensions. The simplest way to create such a rectangle is to use the CGRectMake function that takes four parameters: the x and y coordinates of the upper left corner of the rectangle, then its width and height.
We’ll be creating the rectangles in such a way that their centers coincide with the points through which we’ve just drawn the line graph. Then we’ll inscribe a circle into each of the rectangles, and finally, we’ll stroke and fill the circles. All in all, the code looks like this:
for (int i = 1; i < sizeof(data) - 1; i++)
{
float x = kOffsetX + i * kStepX;
float y = kGraphHeight - maxGraphHeight * data[i];
CGRect rect = CGRectMake(x - kCircleRadius, y - kCircleRadius, 2 * kCircleRadius, 2 * kCircleRadius);
CGContextAddEllipseInRect(ctx, rect);
}
CGContextDrawPath(ctx, kCGPathFillStroke);
Add it to the end of the drawLineGraphWithContext method, and this is the result that you should see:
Quartz 2D Part III Figure 2
Figure 2
The next enhancement we might want to make is filling the space underneath the graph with a color.

Filling the Graph

The steps needed to create a graph filled with color are very similar to those for creating the graph line itself, the only difference is that we need to create a closed path. Let’s see how to do that.
At the beginning of drawLineGraphWithContext method, right after defining the maxGraphHeight, let’s specify a fill color – the same color we are using for drawing the graph itself, but semi-transparent:
CGContextSetFillColorWithColor(ctx, [[UIColor colorWithRed:1.0 green:0.5 blue:0 alpha:0.5] CGColor]);
Then we basically draw the graph, but add a few lines to surround a closed space. Finally, we fill the resulting path but we don’t stroke it. Follow the code and you’ll see what’s going on:
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, kOffsetX, kGraphHeight);
CGContextAddLineToPoint(ctx, kOffsetX, kGraphHeight - maxGraphHeight * data[0]);
for (int i = 1; i < sizeof(data); i++)
{
CGContextAddLineToPoint(ctx, kOffsetX + i * kStepX, kGraphHeight - maxGraphHeight * data[i]);
}
CGContextAddLineToPoint(ctx, kOffsetX + (sizeof(data) - 1) * kStepX, kGraphHeight);
CGContextClosePath(ctx);
CGContextDrawPath(ctx, kCGPathFill);
After the fill is complete, we draw the graph line itself and the circles, as before, on top of it. If you run the app now, you’ll see that the graph became a bit nicer:
Quartz 2D Part III Figure 3
Figure 3
To make it nicer still, we might want to fill the graph with a gradient. You already know how to create a gradient, so it should be easier this second time around.

Filling the Graph with a Gradient

Here is the code that prepares the gradient, with no comments this time. Insert it right before the code that we’ve written in the previous section for creating a solid fill:
CGGradientRef gradient;
CGColorSpaceRef colorspace;
size_t num_locations = 2;
CGFloat locations[2] = {0.0, 1.0};
CGFloat components[8] = {1.0, 0.5, 0.0, 0.2,  // Start color
1.0, 0.5, 0.0, 1.0}; // End color
colorspace = CGColorSpaceCreateDeviceRGB();
gradient = CGGradientCreateWithColorComponents(colorspace, components, locations, num_locations);
CGPoint startPoint, endPoint;
startPoint.x = kOffsetX;
startPoint.y = kGraphHeight;
endPoint.x = kOffsetX;
endPoint.y = kOffsetY;
Now, we are going to reuse the closed path we’ve created in the previous section. This time it will work as a clipping path. Comment out this line:
CGContextDrawPath(ctx, kCGPathFill);
and replace it with the other two:
CGContextSaveGState(ctx);
CGContextClip(ctx);
We are ready to pour the gradient into the graph now. Go ahead:
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, 0);
Finally, do the cleanup:
CGContextRestoreGState(ctx);
CGColorSpaceRelease(colorspace);
CGGradientRelease(gradient);
Here is the result that you should see:
Quartz 2D Part III Figure 4
Figure 4
Not bad, is it? And of course, you can tweak the gradient if you wish.
Now we know how to draw both a bar graph and a line graph. They look good but they are not interactive yet. It would be nice if, say, when the user taps a bar, the graph would display a message appropriate for that bar. This is exactly what we are going to deal with in the next part of the series.

Quartz 2D Index

Alexander Kolesnikov’s series on Creating a Graph using Quartz 2D was split into 5 parts. You can refer to the series using the Quartz 2D Tag and access the individual articles using the links below.