When I joined the team I have been working with recently, they were trying to create a graph using Core Plot, a popular third party library. It didn’t go well though, there were two big problems. First, they couldn’t use a custom image for the graph’s background, as was required by the designer. Second, the quality of scrolling was unacceptable.
By that time, I already had a couple of graphs under my belt, so I suggested an alternative approach: to draw the graph from scratch using nothing else but Quartz 2D – a graphics rendering API created by Apple, a part of the Core Graphics.
At first, this solution didn’t seem optimal. After all, many third party libraries were created to shield developers from using low level APIs, to save our time and effort. However, 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 quite quickly. I spent just a couple of weeks creating a sophisticated solution that implemented every little wish of the designer.
In this series of articles, I am going to share with you the approach I am using for drawing graphs. Let’s put together some requirements for our future solution.
- It should scroll smoothly, and easily resize for different datasets.
- It should respond to touch by displaying an appropriate information.
- It should look well and be flexible enough to make our designers happy.
Creating the Project
We need to create an Xcode project that will host our gradually emerging graph. The View-based Application template will work just fine, and you can give the project any name you find appropriate. I named it MyGraph.After the template project is generated and saved to a location of your choice, our first task is to create a simple user interface. Let’s create a graph that occupies the whole view in landscape orientation, but only the upper part of the view in portrait orientation.
We can decide later to use the lower part of the portrait view for some controls, or for displaying our data as a table. The important requirement is that the graph should scroll in whatever the amount of space is given to it.
In Xcode, select the
MyGraphViewController.xib file
. In the Library, find the Scroll View and drag and drop an instance of it to the main view. With any luck it should snap into place, see the below image to check your progress.Resize the
UIScrollView
so that it occupied the top part of the portrait view and make its height 300 pixels. That’s the height of the landscape view (320 pixels) minus the status bar. Also disable a couple of autosizing handles so that the scroll view resized properly with the change of orientation:For drawing, we are going to use a simple view or, to be precise, a subclass of
UIView
class. In the Library, find View, then drag and drop one right on top of the UIScrollView
. Expand the hierarchy of the objects in the MyGraphViewController.xib
file and make sure that the newly added view became a child of the Scroll View.Also notice that I’ve labeled the two views Main View and Graph View appropriately. This is very easy to do – just press Enter with the view is selected in the hierarchy and give it a new name – but can become incredibly convenient as the number of views in the graph grows.
We’ll want our Graph View to be wide enough, so that it could scroll in the Scroll View, so let’s give it an initial width of 900 pixels, as seen in the image below. This brings the initial setup to completion, and we can start drawing.
Drawing the Grid Lines
We’ll want to start from something simple, something that will just confirm to us that everything is right. A straight line is about the simplest thing one can draw, and most graphs have some sort of grid lines as a visual aid. It would be reasonable then, to start from drawing a bunch of grid lines.But where exactly we are going to draw? So far, our Graph View is just a stock
UIView
that draws nothing. What we need is to extend the UIView
, and in the new class override the drawRect
method: that’s where our drawing code will go.Add to the project a new Objective-C class, make it a subclass of
UIView
and give it a descriptive name, say GraphView
. Next, select the view that we’ve labeled Graph View in the hierarchy of the MyGraphViewController.xib
file and change its class to the new one, GraphView
:In
GraphView.m
, you will see that the - (void)drawRect:(CGRect)rect
method is commented out. Remove the comments and leave the body of the method empty. Before we’ll be able to draw anything, we’ll need to obtain a reference to the graphics context, so let’s add the very first line of code and the method will look like this:- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
}
Our drawing code is going to use a lot of numbers – for example, it will need to know the width and the height of the graph. It is a good idea to define all those numbers as constants in the header file GraphView.h
. This way, whenever you want to change a value, you’ll be able to find it easily. Switch to GraphView.h
and add the following definitions right after the import statement:#define kGraphHeight 300
#define kDefaultGraphWidth 900
#define kOffsetX 10
#define kStepX 50
#define kGraphBottom 300
#define kGraphTop 0
First, we define two constants for the width and the height of the graph, they correspond to the dimensions of the Graph View in the XIB file. We’ll be changing the width of the graph dynamically, but it’s good to have a default value.As for
kStepX
and kOffsetX
, they define the horizontal distance between the vertical grid lines and the offset for the first line respectively.Finally, we define the coordinates for the top and the bottom of the graph. Currently, they are the same as the top and the bottom of the view but we might want to change that with time. (Note that the vertical coordinate starts at the top of the view and increases as we go down.)
We can start drawing now. In Quartz 2D, drawing is done in three steps:
- Preparation. This is where we define which resources and dimensions we are going to use: colors, fonts, dimensions and so on.
- Actual drawing: our code draws lines, curves etc.
- Commit. We need to tell Quartz 2D that we’ve done with this step of drawing and it can make our art visible. If we omit this step, nothing will appear in the view.
drawRect
method:CGContextSetLineWidth(context, 0.6);
CGContextSetStrokeColorWithColor(context, [[UIColor lightGrayColor] CGColor]);
Next, we’ll do the actual drawing of as many vertical lines as can fit in our view:// How many lines?
int howMany = (kDefaultGraphWidth - kOffsetX) / kStepX;
// Here the lines go
for (int i = 0; i < howMany; i++)
{
CGContextMoveToPoint(context, kOffsetX + i * kStepX, kGraphTop);
CGContextAddLineToPoint(context, kOffsetX + i * kStepX, kGraphBottom);
}
Thanks to the descriptive names of Core Graphics functions and our constants, I don’t think there is a need to explain what this code does. Finally, we commit our drawing, and this is the required third step:CGContextStrokePath(context);
Here is the complete code of the drawRect method at this point:- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 0.6);
CGContextSetStrokeColorWithColor(context, [[UIColor lightGrayColor] CGColor]);
// How many lines?
int howMany = (kDefaultGraphWidth - kOffsetX) / kStepX;
// Here the lines go
for (int i = 0; i < howMany; i++)
{
CGContextMoveToPoint(context, kOffsetX + i * kStepX, kGraphTop);
CGContextAddLineToPoint(context, kOffsetX + i * kStepX, kGraphBottom);
}
CGContextStrokePath(context);
}
Further on, I won’t show the complete code anymore as it will be getting longer and longer. Basically, whatever we’ll be adding after this, will always go to the bottom of the method, unless specified otherwise. If you want to see the completed version, you are welcome to checkout the Github BuildMobile Quartz 2D Graph repository.Run the application, and you should see the vertical gray lines in the graph view.
However, when we turn the device to the landscape orientation, the graph doesn’t follow the rotation. We should explicitly declare that we do support change of orientation. In
MyGraphViewController.m
, change the shouldAutorotateToInterfaceOrientation
method so that it looked like this.- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return YES;
}
Try to run the app again, and this time the future graph will follow the rotation of the device.Unfortunately, the graph doesn’t scroll yet. That’s because we need to explicitly tell the
UIScrollView
what’s the size of its content. And to be able to do that, we need to have a pointer to the UIScrollView
.Enabling Scrolling
In Xcode, select theMyGraphViewController.xib
file, make sure that the view hierarchy is visible and press the Show the Assistant editor button in the Editor section of the toolbar. The contents of the MyGraphViewController.h
header file should appear in the right half of the split view. Ctrl-drag from the Scroll View in the hierarchy to the source code on the right hand side so that the insertion point was inside the class declaration, as the following screenshot demonstrates:Release the mouse button, give the new outlet a name, say, scroller, and Xcode will automatically write all the code required to properly obtain a pointer to the Scroll View and maintain it. Here is what the resulting code should look like in
MyGraphViewController.h
. A few lines of code will be added to MyGraphViewController.m
as well.#import <UIKit/UIKit.h>
@interface MyGraphViewController : UIViewController {
UIScrollView *scroller;
}
@property (nonatomic, retain) IBOutlet UIScrollView *scroller;
@end
Now, as soon as the app is ready to be displayed, we need to tell the Scroll View what’s the size of its child Graph View. The best place to do that is in the viewDidLoad
method of the MyGraphViewController
class. This method is probably already present in MyGraphViewController.m
but it might be commented out. Remove the comments and add the following line of code:- (void)viewDidLoad
{
[super viewDidLoad];
scroller.contentSize = CGSizeMake(kDefaultGraphWidth, kGraphHeight);
}
Now you can run the app, and the Graph View will scroll nicely, in both portrait and landscape orientation.Tweaking the Grid Lines
To be honest with you, I decided to draw the grid lines before enabling scrolling so that you could easily notice whether or not the graph needed to be scrolled. Now that the grid lines are there, we might want to tweak them a little bit.One obvious addition is the horizontal grid lines. That’s easy. First add a couple of new constant definitions to
GraphView.h
#define kStepY 50
#define kOffsetY 10
Then add the following lines of code right after drawing the vertical grid lines, before the line that commits the drawing.int howManyHorizontal = (kGraphBottom - kGraphTop - kOffsetY) / kStepY;
for (int i = 0; i <= howManyHorizontal; i++)
{
CGContextMoveToPoint(context, kOffsetX, kGraphBottom - kOffsetY - i * kStepY);
CGContextAddLineToPoint(context, kDefaultGraphWidth, kGraphBottom - kOffsetY - i * kStepY);
}
If you run the application, you will see both the vertical and the horizontal grid lines.In many cases, this kind of grid line will be good enough, but your designers might not be happy until you make the lines dashed. Thankfully, we can do that easily by simply adding a couple of lines of code to the preparation step. Right underneath the line that sets the stroke color, add the following code.
CGFloat dash[] = {2.0, 2.0};
CGContextSetLineDash(context, 0.0, dash, 2);
The dash
array specifies that there are two elements in the pattern: a dash and an empty space after it. The last parameter of the CGContextSetLineDash
function, 2
, is the number of elements in the dash array. Knowing this, you can experiment with different types of dash patterns to create something that is appropriate for your purposes. If you run this code as it is, you should see the grid lines in the following screenshot.Any other lines that we are going to draw later won’t be dashed, so we need to disable the dash that we’ve set up before. To do that, insert the following line of code right after the line that commits the drawing.
CGContextSetLineDash(context, 0, NULL, 0); // Remove the dash
Now we’ve prepared everything we might need before actually drawing the graph. The only other enhancement we might want to add is a graphical background for the graph, possibly crafted by our designers.Adding a Graphical Background
I am not a designer, so the background I’ve created is very simple. The file is namedbackground.png
, and you will find it in the code on GitHub. Alternatively, you can create a graphic of your own, it should be 300 pixels tall and 900 pixels wide.Add the background file to the project. I have created a new group named Artwork and placed the file into this group. Next, we need to insert three lines of code before and grid line drawing was done. Put them after the first line of this method, the one where we obtain a reference to the graphics context:
CGContextRef context = UIGraphicsGetCurrentContext();
// Draw the background image
UIImage *image = [UIImage imageNamed:@"background.png"];
CGRect imageRect = CGRectMake(0, 0, image.size.width, image.size.height);
CGContextDrawImage(context, imageRect, image.CGImage);
...
Run the project, and you will see both the background and the grid lines:Admittedly, using both a graphical background and grid lines in this particular case looks like an overkill. You might decide to leave either the background or the grid lines, but at least you know how to draw both.
Note: In this particular case, the background is very simple. If it was a more complex drawing, you would notice that it’s being drawn upside down. We’ll learn how to deal with this later in the article, for now let’s just leave it as it is.
Now that all the background work is completed, we can start drawing the actual graph, and this is exactly what we shall do in the next part of the article. Grab the Quartz 2D Graph code from the GitHub Repo and leave us your thoughts and questions in the comments. See you soon.
没有评论:
发表评论