This chapter is from the book

This chapter is from the book

This chapter will focus on customizing the user interface. First, we will look at drawing. By drawing our own views, we can create a wide range of visual effects—and by extension, a wide range of custom controls. Then we will look at setting the transitions between scenes and providing animations for those transitions.

In previous versions of iOS, custom drawing grew to become a large part of many applications. As developers tried to give their applications a unique look and feel or tried to push beyond the boundaries of the UIKit controls, they began to increasingly rely on custom drawing to make their applications stand out.

However, iOS 7 is different. In iOS 7, Apple recommends pushing the application’s user interface into the background. It should, for the most part, disappear, focusing attention on the user’s data rather than drawing attention to itself. As a result, the developers are encouraged to create their own unique look and feel through less visually obvious techniques. In this new paradigm, custom animations may replace custom interfaces as the new distinguishing feature.

Custom Drawing

In previous chapters, we set up the GraphViewController and made sure that it received a copy of our weightHistoryDocument. However, it is still just displaying an empty, white view. Let’s change that. We’re going to create a custom view with custom drawing—actually, to more easily handle the tab bar and status bar, we will be creating two custom views and split our drawing between them.

Let’s start with the background view. This will be the backdrop to our graph. So, let’s make it look like a sheet of graph paper.

Drawing the Background View

Create a new Objective-C class in the Views group. Name it BackgroundView, and make it a subclass of UIView. You should be an expert at creating classes by now, so I won’t walk through all the steps. However, if you need help, just follow the examples from previous chapters.

There are a number of values we will need when drawing our graph grid. For example, how thick are the lines? How much space do we have between lines, and what color are the lines?

We could hard-code all this information into our application; however, I prefer to create properties for these values and then set up default values when the view is instantiated. This gives us a functional view that we can use right out of the box—but also lets us programmatically modify the view, if we want.

Open BackgroundView.h. Add the following properties to the header file:

Now, open BackgroundView.m. The template automatically creates an initWithFrame: method for us, as well as a commented-out drawRect:. We will use both of these.

Let’s start by setting up our default values. Views can be created in one of two ways. We could programmatically create the view by calling alloc and one of its init methods. Eventually, this will call our initWithFrame: method, since that is our designated initializer. More likely, however, we will load the view from a nib file or storyboard. In this case, initWithFrame: is not called. The system calls initWithCoder: instead.

To be thorough, we should have both an initWithFrame: method and an initWithCoder: method. Furthermore, both of these methods should perform the same basic setup steps. We will do this by moving the real work to a private method that both init methods can then call.

awakeFromNib

When loading objects from a nib, you may want to perform any additional configuration and setup in the object’s awakeFromNib method. Remember, initWithCoder: is called when the object is first instantiated. Then all of its outlets and actions are connected. Finally, awakeFromNib is called. This means you cannot access any of the object’s outlets in the initWithCoder: method. They are, however, available in awakeFromNib.

In our case, we don’t have any outlets. Also, I like the symmetry between initWithFrame: and initWithCoder:. Still, awakeFromNib can be a safer location for additional configuration.

We discussed initWithCoder: in the “Secret Life of Nibs” section of Chapter 2, “Our Application’s Architecture.” It is similar to, but separate from, our regular initialization chain. Generally speaking, initWithCoder: is called whenever we load an object from disk, and nibs and storyboards get compiled into binary files, which the system loads at runtime. Not surprising, we will see initWithCoder: again in Chapter 5, “Loading and Saving Data.”

Now, we just need to create our setDefaults method. Create the following code at the bottom of our implementation block:

We start by setting our view’s background color to clear. This will override any background color we set in Interface Builder. I often use this trick when working with custom views, since they will not get drawn in Interface Builder. By default, all views appear as a rectangular area on the screen. Often we need to set a temporary background color, since the views otherwise match the parent’s background color or are clear (letting the parent view show through). This, effectively, makes them invisible in Interface Builder.

By setting this property when the view is first loaded, we can freely give it any background color we want in Interface Builder. This make our custom views easier to see while we work on them. Plus, by setting the value when the view is created, we can still override it programmatically in the controller’s viewDidLoad if we want.

Next, we set the view to opaque. This means we promise to fill the entire view from corner to corner with opaque data. In our case, we’re providing a completely opaque background color. If we programmatically change the background color to a translucent color, we should also change the opaque property.

Opaque Views

Each view has an opaque property. This property acts as a hint to the drawing system. If the property is set to YES, the drawing system will assume the view is completely opaque and will optimize the drawing code appropriately. If it is set to NO, the drawing system will composite this view with any underlying views. Typically, this means checking pixel by pixel and blending multiple values for each pixel, if necessary. This allows for more elaborate visual effects—but also increases the computational cost.

By default, opaque is set to YES. We should avoid changing this unless we actually need transparent or translucent areas within our view.

However, having an opaque view means our custom drawing code must completely fill the provided rectangle. We cannot leave any transparent or semi-transparent regions that might let underlying views show through. The simplest solution is to assign an opaque backgroundColor, which will implicitly fill the entire view’s rectangle.

Next, we check the view’s scale factor. The default value we use for our line width, the x-offset and the y-offset, all vary depending on whether we are drawing to a regular or a Retina display.

All the iPhones and iPod touches that support iOS 7 have Retina displays. However, the iPad 2 and the iPad mini still use regular displays—so it’s a good idea to support both resolutions. After all, even if we are creating an iPhone-only app, it may end up running on an iPad.

Additionally, Apple may release new devices that also have non-Retina displays. The iPad mini is a perfect example. It has the same resolution as a regular size iPad 2; it just shrinks the pixel size down by about 80 percent. Also, like the iPad 2, it uses a non-Retina display, presumably to reduce cost and improve performance and energy use.

We want to draw the smallest line possible. This means the line must be 1-pixel wide, not 1-point wide. We, therefore, have to check the scale and calculate our line width appropriately. Furthermore, we also need to make sure our lines are properly aligned with the underlying pixels. That means we have to offset them by half the line’s width (or, in this case, half a pixel).

Now we need to draw our grid. Uncomment the drawRect: method. The system calls this method during each display phase, as discussed in Chapter 3, “Developing Views and View Controllers.” Basically, each time through the run loop, the system will check all the views in the view hierarchy and see whether they need to be redrawn. If they do, it will call their drawRect: method.

Also as mentioned in the previous chapter, our views aggressively cache their content. Therefore, most normal operations (changing their frame, rotating them, covering them up, uncovering them, moving them, hiding them, or even fading them in and out) won’t force the views to be redrawn. Most of the time this means our view will be drawn once when it is first created. It may be redrawn if our system runs low on memory while it’s offscreen. Otherwise, the system will just continue to use the cached version.

Drawing for the Retina Display

As you probably know, the iPhone’s Retina display has a 960x640 display. This is four times the number of pixels as the 3GS and earlier models. This could result in a lot of complexity for developers—if we had to constantly test for the screen size and alter our drawing code to match. Fortunately, Apple has hidden much of this complexity from us.

All the native drawing functions (Core Graphics, UIKit, and Core Animation) use a logical coordinate system measured in points. A point is approximately 1/160 of an inch. It’s important to note that these points may or may not be the same as the screen’s pixels. Draw a 1-point-wide line on a regular display, and you get a 1-pixel-wide line. Draw the same line on a Retina display, and it is now 2 pixels wide. This also means a full-screen frame is the same size, regardless of the display type. We still have three screen sizes to deal with: iPhone 3.5-inch, iPhone 4-inch, and iPad. But, three is better than five or six.

A device’s scale factor gives us the conversion between points and pixels. You can access the scale property from the UIScreen, UIView, UIImage, or CALayer classes. This allows us to perform any resolution-dependent processing. However, we actually get a lot of support for free.

All standard UIKit views are automatically drawn at the correct resolution.

There are, however, some steps we still need to take to fully support multiple screen resolutions. One obvious example occurs when loading and displaying images and other bitmapped art. We need to create higher-resolution copies of these files for the Retina display. Fortunately, UIKit supports automatically loading the correct resolution from the assets category.

Let’s say you have a 20-pixel by 30-pixel image named stamp.png. You also need to create a Retina version that is 40 x 60 pixels. Now open the Images.xcassets. The catalog editor has two parts: a list of all your assets on the left and a detail view on the right.

Drag and drop your original image into the assets list. Xcode will automatically make a new entry for it called stamp. Now select the stamp entry. The detail view will show you the 1x version of your image, with a placeholder for the 2x. Drag the Retina version into the 2x position. The image is now ready to use. You just need to request the image by the asset’s name, as shown here:

UIImage* stampImage =
[UIImage imageNamed:@”stamp”];

UIKit will automatically load the correct version for your device.

Similarly, if we are creating bitmaps programmatically, we will want to make sure we give them the correct scale factor. The UIGraphicsBeginImageContext function creates a bitmap-based graphics context with a 1.0 scale factor. Instead, use the UIGraphicsBeginImageContextWithOptions function and pass in the correct scale (which you can access by calling [[UIScreen mainScreen] scale]).

Core Animation layers may also need explicit support. Whenever you create a new CALayer (one that is not already associated with a view), it comes with a default scale value of 1.0. If you then draw this layer onto a Retina display, it will automatically scale up to match the screen. You can prevent this by manually changing the layer’s scale factor and then providing resolution-appropriate content.

Finally, OpenGL ES also uses a 1.0 scale by default. Everything will still draw on the Retina display, but it will be scaled up and may appear blocky (especially when compared to properly scaled drawings). To get full-scale rendering, we must increase the size of our render buffers. This, however, can have severe performance implications. Therefore, we must make these changes manually. Changing a view’s contentScaleFactor will automatically alter the underlying CAEAGLLayer, increasing the size of the render buffers by the scale factor.

It is also important to test your drawing code on all the devices you intend to support. Drawing a 0.5-point-wide line may look fine on a retina display but appear large and fuzzy on a regular display. Similarly, higher-resolution resources use more memory and may take longer to load and process. This is particularly true for full-screen images.

Even with all these exceptions and corner cases, iOS’s logical coordinate system greatly simplifies creating custom drawing code that works across multiple resolutions. Most of the time we get automatic Retina support without doing anything at all.

Subpixel Drawing

All of our drawing routines use CGFloat, CGPoint, CGSize, and CGRect, and all of these use floating-point values. That means we can draw a 0.34-pixel-wide line at y = 23.428. Obviously, however, our screen cannot display partial pixels.

Instead, our drawing engine simulates subpixel resolutions using anti-aliasing—proportionally blending the drawn line with the background color in each partial pixel. Anti-aliasing makes lines look smoother, especially curved or diagonal lines. It fills in the otherwise harsh, jagged edges. Unfortunately, it can also make our drawings appear softer or somewhat fuzzy.

By default, anti-aliasing is turned on for any window or bitmap context. It is turned off for any other graphics contexts. However, we can explicitly set our context’s anti-aliasing using the CGContextSetShouldAntiAlias() function.

It’s also important to realize that the graphics context’s coordinates fall in between the pixels, and the drawing engine centers the lines on their endpoints. For example, if you draw a 1-point-wide horizontal line at y = 20, on a non-Retina display (iPad 2 or iPad mini, for example), the line will actually be drawn half in pixel row 19 and half in row 20. If anti-aliasing is turned on, this will result in two rows of pixels, each with a 50 percent blend. If anti-aliasing is off, you will get a 2-pixel-wide line. You can fix this by offsetting the drawing coordinates by half a point (y = 20.5). Then the line will fall exactly along the row of pixels.

For a Retina display, the 1-point line will result in a 2-pixel line that properly fills both rows on either side of the y-coordinate. A 0.5 pixel-wide line, though, will similarly need to be offset by 0.25 pixels. For more information on how Retina displays and scale factors work, see the “Drawing for the Retina Display” sidebar.

Additionally, when the system does ask us to draw or redraw a view, it typically asks us to draw the entire view. The rect passed into drawRect: will be equal to the view’s bounds. The only time this isn’t true is when we start using tiled layers or when we explicitly call setNeedsDisplayInRect: and specify a subsection of our view.

NOTE

We should never call drawRect: directly. Instead, we can force a view to redraw its contents by calling setNeedsDisplay or setNeedsDisplayInRect:. This will force the view to be redrawn the next time through the run loop. setNeedsDisplay will update the entire view, while setNeedsDisplayInRect: will redraw only the specified portion of the view.

Drawing is expensive. If you’re creating a view that updates frequently, you should try to minimize the amount of drawing your class performs with each update. Write your drawRect: method so that it doesn’t do unnecessary drawing outside the specified rect. You should also use setNeedsDisplayInRect: to redraw the smallest possible portion. However, for Health Beat, our views display largely static data. The graph is updated only when a new entry is added. As a result, we can take the easier approach, disregard the rect argument, and simply redraw the entire view.

We start by using some of the CGGeometry functions to extract the width and height from our view’s bounds. Then we create an empty UIBezierPath.

Bezier paths let us mathematically define shapes. These could be open shapes (basically lines and curves) or closed shapes (squares, circles, rectangles, and so on). We define the path as a series of lines and curved segments. The lines have a simple start and end point. The curves have a start and end point and two control points, which define the shape of the curve. If you’ve ever drawn curved lines in a vector art program, you know what I’m talking about. We can use our UIBezierPath to draw the outline (stroking the path) or to fill in the closed shape (filling the path).

UIBezierPath also has a number of convenience methods to produce ready-made shapes. For example, we can create paths for rectangles, ovals, rounded rectangles, and arcs using these convenience methods.

For our grid, the path will just be a series of straight lines. We start with an empty path and then add each line to the path. Then we draw it to the screen.

Notice, when we create the path, we set the line’s width. Then we create a line every 20 points, both horizontally and vertically. We start by moving the cursor to the beginning of the next line by calling moveToPoint:, and then we add the line to the path by calling addLineToPoint:. Once all the lines have been added, we set the line color by calling UIColor’s setStroke method. Then we draw the lines by calling the path’s stroke method.

We still need to tell our storyboard to use this background view. Open Main.storyboard and zoom in on the Graph View Controller scene. Select the scene’s view object (either in the Document Outline or by clicking the center of the scene), and in the Identity inspector, change Class to BackgroundView.

Just like in Chapter 3, the class should be listed in the drop-down; however, sometimes Xcode doesn’t correctly pick up all our new classes. If this happens to you, you can just type in the name, or you can quit out of Xcode and restart it—forcing it to update the class lists.

Run the application and tap the Graph tab. It should display a nice, graph paper effect (Figure 4.1).

There are a couple of points worth discussing here. First, notice how our drawing stretches up under the status bar. Actually, it’s also stretched down below the tab bar, but the tab bar blurs and fades the image quite a bit, so it’s not obvious.

One of the biggest changes with iOS 7 is the way it handles our bars. In previous versions of iOS, our background view would automatically shrink to fit the area between the bars. In iOS 7, it extends behind the bars by default.

We can change this, if we want. We could set the view controller’s edgesForExtended Layout to UIRectEdgeNone. In our view, this would prevent our background view from extending below the tab bar. It will also make the tab bar opaque. However, this won’t affect the status bar.

We actually want our background view to extend behind our bars. It gives our UI a sense of context. However, we want the actual content to be displayed between the bars. To facilitate this, we will draw our actual graph in a separate view, and we will use auto layout to size that view properly between the top and bottom layout guides.

Also notice, the status bar does not have any background color at all. It is completely transparent. We can set whether it displays its contents using a light or a dark font—but that’s it. If we want to blur or fade out the content behind the bar, we need to provide an additional view to do that for us. However, I will leave that as a homework exercise.

There is one slight problem with our background view, however. Run the app again and try rotating it. When we rotate our view, auto layout will change its frame to fill the screen horizontally. However, as I said earlier, this does not cause our view to redraw. Instead, it simply reuses the cached version, stretching it to fit (Figure 4.2).

We could fix this by calling setNeedsDisplay after our view rotates—but there’s an easier way. Open BackgroundView.m again. Find setDefaults, and add the following line after self.opaque = YES:

self.contentMode = UIViewContentModeRedraw;

The content mode is used to determine how a view lays out its content when its bounds change. Here, we are telling our view to automatically redraw itself. We can still move the view, rotate it, fade it in, or cover it up. None of that will cause it to redraw. However, change the size of its frame or bounds, and it will be redrawn to match.

Run the app again. It will now display correctly as you rotate it.

Drawing the Line Graph

Now let’s add the line graph. Let’s start by creating a new UIView subclass named GraphView. Place this in the Views group with our other views.

Before we get to the code, let’s set up everything in Interface Builder. Open Main.storyboard. We should still be zoomed into the Graph View Controller scene.

Drag a new view out and place it in the scene. Shrink it so that it is a small box in the middle of the scene and then change its Class to GraphView and its background to light Gray. This will make it easier to see (Figure 4.3).

We want to resize it so that it fills the screen from left to right, from the top layout guide to the bottom. However, it will be easier to let Auto Layout do that for us. With the graph view selected, click the Pin tool. We want to use the Add New Constraints controls to set the constraints shown in Table 4.1. Next, set Update Frames to Items of New Constraints, and click the Add 4 Constraints button. The view should expand as desired.

TABLE 4.1 Graph View’s Constraints

Side

Target

Constant

Left

Background View

0

Top

Top Layout Guide

0

Right

Background View

0

Bottom

Background View*

49*

* Ideally, we would like to pin the bottom of our graph view to the Bottom Layout Guide with a spacing constant of 0. This should align the bottom of our graph view with the top of the tab bar. If the tab bar’s height changes or if we decide to remove the tab bar, the Bottom Layout Guide would automatically adjust.However, as of the Xcode 5.0 release, there is a bug in the Bottom Layout Guide. When the view first appears it begins aligned with the bottom of the background view, not the top of the tab bar. If you rotate the view, it adjusts to the correct position. Unfortunately, until this bug is fixed, we must hard-code the proper spacing into our constraints. That is the only way to guarantee that our graph view gets laid out properly.

Run the app and navigate to our graph view. It should fill the screen as shown in Figure 4.4. Rotate the app and make sure it resizes correctly.

There’s one last step before we get to the code. We will need an outlet to our graph view. Switch to the Assistant editor, Control-drag from the graph view to GraphViewController.m’s class extension, and create the following outlet:

@property (weak, nonatomic) IBOutlet GraphView *graphView;

Unfortunately, this creates an “Unknown type name ‘GraphView’” error. We can fix this by scrolling to the top of GraphViewController.m and importing GraphView.h.

Now, switch back to the Standard editor, and open GraphView.h. We want to add a number of properties to our class. First, we need an NSArray to contain the weight entries we intend to graph. We also need to add a number of properties to control our graph’s appearance. This will follow the general pattern we used for the background view.

Notice that we’re assuming our weight entries are sorted in ascending order. When we implement our code, we’ll add a few sanity checks to help validate the incoming data—but we won’t do an item-by-item check of our array. Instead, we will assume that the calling code correctly follows the contract expressed in our comment. If it doesn’t, the results will be unpredictable.

We’ve seen this methodology before. Objective-C often works best when you clearly communicate your intent and assume other developers will follow that intent. Unfortunately, in this case, we don’t have any syntax tricks that we can use to express our intent. We must rely on comments. This is, however, an excellent use case for documentation comments. I’ll leave that as an exercise for the truly dedicated reader.

Switch to GraphView.m. Just like our background view, we want the pair of matching init... methods, with a private setDefaults method. The init... methods are shown here:

This code is almost the same as our background view. Yes, the property names and values have changed, but the basic concept is the same. Notice that we are using a clear backgroundColor, letting our background view show through. We have also set the opaque property to YES.

Additionally, since our graph line’s width is an even number of pixels, we don’t need to worry about aligning it to the underlying pixels. If it’s drawn as a vertical or horizontal line, it will already correctly fill 1 pixel above and 1 pixel below the given coordinates (in a non-Retina display). By a similar logic, the 1-point guidelines need to be offset only in non-Retina displays.

The app should successfully build and run. However, notice that our previously gray graph view has completely vanished. Now we just need to add some custom drawing, to give it a bit of content.

For the purpose of this book, we want to keep our graph simple. So, we will just draw three horizontal guidelines with labels and the line graph itself. Also, we’re not trying to match the graph with the background view’s underlying graph paper. In a production app, you would probably want to add guidelines or labels for the horizontal axis, as well as coordinate the background and graph views, giving the graph paper actual meaning.

Passing the Data

The graph will be scaled to vertically fill our space, with the maximum value near the top of our view and the minimum near the bottom. It will also horizontally fill our view, with the earliest dates close to the left side and the latest date close to the right.

But, before we can do this, we need to pass in our weight entry data. Open GraphViewController.m. First things first—we need to import WeightHistoryDocument.h. We already added a forward declaration in the .h file, but we never actually imported the class. It hasn’t been necessary, since we haven’t used it—until now.

As our GraphView’s comments indicated, we want to pass our weight entries in ascending order. However, our document stores them in descending order. So, we have to reverse this array.

We grab all the entries from our weight history document. We then access the reverse enumerator for these entries. The reverse enumerator lets us iterate over all the objects in the reverse order. Finally we call the enumerator’s allObjects method. This returns the remaining objects in the collection. Since we haven’t enumerated over any objects yet, it returns the entire array. This is a quick technique for reversing any array.

Of course, we could have just passed the entire weight history document to our graph view, but isolating an array of entries has several advantages. First, as a general rule, we don’t want to give any object more access to our model than is absolutely necessary. More pragmatically, writing the code this way gives us a lot of flexibility for future expansions. We could easily modify our code to just graph the entries from last week, last month, or last year. We’d just need to filter our entries before we passed them along.

Unfortunately, there is one small problem with this approach. WeightHistoryDocument doesn’t have an allEntries method. Let’s fix that. Open WeightHistoryDocument.h and declare allEntries just below weightEntriesAfter:before:.

- (NSArray *)allEntries;

Now switch to WeightHistoryDocument.m. Implement allEntries as shown here:

- (NSArray *)allEntries
{
return [self.weightHistory copy];
}

Here, we’re making a copy of our history array and returning the copy. This avoids exposing our internal data, and prevents other parts of our application from accidentally altering it.

NOTE

We have two options when it comes to returning our internal array. We could create an immutable copy of our array and return it, as shown here. This is the safest approach. Unfortunately, it can get very expensive, especially once our array grows quite large. Alternatively, we could just return the weightHistory array unmodified. While this may seem risky, our method signature says that we’re returning an NSArray. Therefore, anyone calling this method should treat the returned value as an immutable array. Calling NSMutableArray methods on it would violate our contract. Since we call this method only once, when our view appears, the more secure approach is probably better. However, if we had to call this method multiple times in a tight loop, we might want to switch and just return our internal data directly. As always, if you have doubts, profile your application and base your decision on hard data.

Now, switch back to GraphView.m. There are a number of private properties we want to add. Some of these will hold statistical data about our entries. Others will hold layout data—letting us easily calculate these values once and then use them across a number of different drawing methods.

At the top of GraphView.m, import WeightEntry.h, and then add a class extension as shown here:

Ideally, nothing in this method comes as a surprise. We assign the incoming value to our property. Then we check to see whether our new array has any elements. If it does, we set up our statistical data. Here, we’re using the KVC operators we used in the “Updating Our View” section of Chapter 3. Notice that we can use them for both the weight and the date properties. @min.date gives us the earliest date in the array, while @max.date gives us the latest. We also check the first and last object and make sure the first object is our earliest date and the last object is our latest date. This acts as a quick sanity check. Obviously it won’t catch every possible mistake—but it is fast, and it will catch most obvious problems (like forgetting to reverse our array). As a bonus, it works even if our array has only one entry.

If our array is empty (or has been set to nil), we simply clear out the stats data.

Now, let’s draw the graph. Drawing code can get very complex, so we’re going to split it into a number of helper methods.

Start by adding methods to calculate our graph size. We’ll be storing these values in instance variables. I’m always a bit uncomfortable when I store temporary data in an instance variable. I like to clear it out as soon as I’m done—preventing me from accidentally misusing the data when it’s no longer valid. So, let’s make a method to clean up our data as well.

The calculateGraphSize shows off more of the CGGeometry functions. Here, we use CGRectInset to take a rectangle and shrink it. Each side will be moved in by our margin. Given our default values, this will create a new rectangle that is centered on the old one but is 40 points shorter and 40 points thinner.

You can also use this method to make rectangles larger, by using negative inset values.

Then we use the CGRectGet... methods to pull out various bits of data. The topY value deserves special attention. We want the top margin to be twice as big as the bottom, left, and right. Basically, we want all of our content to fit inside the innerBounds rectangle. We will use the topY and bottomY to draw our minimum and maximum weight guidelines. However, the top guideline needs a little extra room for its label.

The clearGraphSize method is much simpler; we just set everything to 0.0.

We start by checking to see whether we have any entries. If we don’t have any entries, we’re done. There’s no graph to draw.

If we do have entries, we create an empty Bezier path to hold our line graph. We iterate over all the entries in our array. We calculate the correct x- and y-coordinates for each entry, based on their weight and date. Then, for the first entry we move the cursor to the correct coordinates. For every other entry, we add a line from the previous coordinates to our new coordinates. If our entry is our minimum or maximum value, we draw a dot at that location as well. Finally we draw the line. This time, we’re using our view’s tintColor. We discussed tintColor in the “Customizing Our Appearance” section of Chapter 3. Basically, graphView’s tintColor will default to our window’s tint color—but we could programmatically customize this view by giving it a different tintColor.

Let’s add the methods to calculate our x- and y-coordinates. Start with the x-coordinate.

We start with a quick sanity check. This method should never be called if we don’t have any entries. Then we check to see whether our earliest date and our latest date are the same. If they are (for example, if we have only one date), they should be centered in our graph. Otherwise, we need to convert our dates into numbers that we can perform mathematical operations on. The easiest way to do this is using the timeIntervalSinceDate: method. This will return a double containing the number of seconds between the current date and the specified dates.

We can then use these values to calculate each date’s relative position between our earliest and latest dates. We calculate a percentage from 0.0 (the earliest date) to 1.0 (the latest date). Then we use that percentage to determine our x-coordinate.

Note that our literal floating-point values and our time intervals are doubles. If we compile this on 32-bit, we want to convert them down to floats. If we compile on 64-bit, we want to leave them alone. We can accomplish this by casting them to CGFloat—since its size changes with the target platform.

The Y value calculations are similar. On the one hand, the code is a little simpler, since we can do mathematical operations on the weight values directly. Notice, however, that the math to calculate our percentage is much more complicated. There are two reasons for this. First, in our dates, the earliest date will always have a value of 0.0. With our weights, this is not true. We need to adjust for our minimum weight. Second, remember that our y-coordinates increase as you move down the screen. This means our maximum weight must have the smallest y-coordinate, while our minimum weight has the largest. We do this by inverting our percentage, subtracting the value we calculate from 1.0.

Here we get the x- and y-coordinates for our entry and calculate a bounding box for our dot. Our bounding box’s height and width are equal to our dotSize property, and it is centered on our x- and y-coordinates. We then call bezierPathWithOvalInRect: to calculate a circular Bezier path that will fit inside this bounding box. Then we draw the dot, using our view’s tint color as the fill color.

Now, we just need to call these methods. Replace the commented out drawRect: with the version shown here:

Build and run the application. When it has zero entries, the graph view is blank. If you add one entry, it will appear as a dot, roughly in the center of the screen. Add two or more entries, and it will draw the graph’s line (Figure 4.5). Rotate it, and the graph will be redrawn in the new orientation.

We start by getting the Dynamic Type font for the Caption 1 style. Then we create a dictionary of text attributes. These attributes are defined in NSAttributedString UIKit Addons Reference. They can be used for formatting parts of an attributed string; however, we will be using them to format our entire string. We’re setting the font to our Dynamic Type font and setting the text color to the guideline color.

Once that’s done, we use these attributes to calculate the size needed to draw our text. Then we create a rectangle big enough to hold the text and whose bottom edge is 1 point above the provided y-coordinate. Notice that we use CGRectIntegral() on our rectangle. This will return the smallest integral-valued rectangle that contains the source rectangle.

The sizeWithAttributes: method typically returns sizes that include fractions of a pixel. However, we want to make sure our rectangle is properly aligned with the underlying pixels. Unlike the custom line drawing, when working with text, we should not offset our rectangle—the text-rendering engine will handle that for us.

We first use the textRect to draw a box using our BackgroundView’s background color. This will create an apparently blank space for our label, making it easier to read. Then we draw our text into the same rectangle.

Next, we define our line. Again, we start with an empty path. We set the line’s width, and we set the dash pattern. Here, we’re using a C array. This is one of the few places where you’ll see a C array in Objective-C. Our dashed line pattern will draw a segment 5 points long, followed by a 2-point gap. It then repeats this pattern.

We draw this line from the left edge of our view to the right edge of our view along the provided y-coordinate. This means our line will lay just below the bottom edge of our text’s bounding box. Finally, we set the line color and draw the line.

Now we need to create our guidelines. We will have three: one for the max weight, one for the minimum weight, and one for the average weight. Add the following method to our implementation file:

In this method, we simply check to see whether we don’t have any entries. If there are no entries, then we don’t need to draw any guidelines, so we just return. Next, we check to see whether all of our entries have the same weight (either because we have only one entry or because the user entered the same weight for all entries). If we have only one weight value, we create a single weight label and draw our line at the calculated y-coordinate (which should be the vertical center of the graph). Our line should match up with the dot or line that we drew in our drawGraph method.

If we have more than one weight value, we create a minimum, maximum, and average strings and then draw lines corresponding to the appropriate values.

Run the application. With no entries, the graph view is still blank. Add a single entry. This will appear in roughly the middle of the screen, with a guideline running right through our dot. Add multiple lines. You should see the Max, Average, and Min guidelines (Figure 4.6).

If the user changes the font size, this method will mark our graph view as needing to be redrawn. The next time through the run loop, the system will redraw the graph, picking up the new fonts in the process.

Run the app. It will now update correctly when the user changes the font size.

Now, as you can see, there’s a lot of room for improvement in this graph. Ideally, however, you’ve learned a lot along the way. We’ve covered drawing lines (the grid, graph, and guide lines), shapes (the dots), and text. There are similar methods for drawing images as well—though, it is often easier to just insert a UIImageView into your application. The UIImageView’s drawing code is highly optimized, and you’re unlikely to match its performance.

You should be able to use these, and the other UIKit drawing methods, to create your own custom views. However, it doesn’t end here. There are a couple of other graphic techniques worth considering.

Other Graphics Techniques

Under the hood, all of the UIKit drawing methods are actually calling Core Graphics methods. Core Graphics is a low-level C-based library for high-performance drawing. While, most of the time, we can do everything we want at the UIKit level—if you need special features (e.g., gradients or shadows), you will need to switch to Core Graphics.

All core graphics functions take a CGContextRef as their first argument. The context is an opaque type that represents the drawing environment. Our UIKit methods also draw into a context—however, the context is hidden from us by default. We can access it, if we need, by calling UIGraphicsGetCurrentContext(). This lets us freely mix UIKit and Core Graphics drawing code.

In the drawRect: method, our code is drawn into a context that is then displayed on the screen, we can also create graphics contexts to produce JPEG or PNG images or PDFs. We may even use graphics contexts when printing.

However, custom drawing isn’t the only way to produce a custom user interface. We can also drop bitmap images into our layout. There are two main techniques for adding bitmaps. If it’s simply artwork, we can just place a UIImageView in our view hierarchy and set its image property. If, however, our image needs to respond to touches, we can place a custom UIButton and then call its backgroundImageForState: method (or set the background image in Interface Builder).

Bitmaps are often easier than custom drawing. You simply ask your graphic designer to create them for you. You place them in the assets catalog, and you drop them into your interface.

However, custom drawn interfaces have a number of advantages. The biggest is their size. Custom drawn views require considerably less memory than bitmaps. The custom drawn views can also be redrawn to any arbitrary size. If we want to change the size of a bitmapped image, we usually need to get our graphic designer to make a new one. Finally, custom drawn views can be dynamically generated at runtime. Bitmaps can only really present static, unchanging pieces of art.

Often, however, we can get many of the advantages of bitmaps while sidestepping their weaknesses by using resizeable images or image patterns. For more information, check out -[UIImage resizableImageWithCapInsets:], +[UIColor colorWithPatternImage:}, and the Asset Catalog Help.

NOTE

When using bitmapped images, you should always use PNG files, if possible. Xcode will optimize the PNG files for the underlying iOS hardware during compile time. Other formats may require additional processing at runtime, which may cause noticeable sluggishness or delays, especially for large files.

Updating Our Drawing When the Data Changes

There’s one last modification to our graph view. We need to make sure our graph is updated whenever our data changes. This isn’t so important now, but it will become extremely important once we implement iCloud syncing.

To do this, we need to listen for our model’s notifications. However, unlike our history view, we don’t need to track each change individually. Instead, we will update our entire graph once all the changes are complete. This means we need to track only a single notification.

Open GraphViewController.m. In the class extension, add the following property: