To create your own view class, you need to understand three things:
n The view coordinate system
n User interface update events
n How to draw in a Core Graphics context
Let’s start at the top—literally.
View Coordinates
The device’s screen, windows, and views all have a graphics coordinate system. The coordinate system establishes the position and size of everything you see on the device: the screen, windows, views, images, and shapes.Every view object has its own coordinate system. The origin of a coordinate system is at its upper-left corner and has the coordinates (0,0), as shown in Figure 11-1.
Figure 11-1. Graphics coordinate system
X coordinates increase to the right and Y coordinates increase downward. The Y-axis is upside-down from the Cartesian coordinate system you learned in school, or maybe from reading geometry books in your spare time. Forcomputer programs, this arrangement is more convenient; most content “flows” from the upper-left corner, so it’s usually simpler to perform calculations from the upper-left corner than the lower-left corner.
There are four key variable types used to describe coordinates, positions, sizes, and areas in iOS, all listed in Table 11-1.
Table 11-1. Coordinate value types
Type Description
CGFloat The fundamental
scalar value. CGFloat is floating point type used
to express a single coordinate or distance.
CGPoint A pair of CGFloat values that specify a point (x,y) in a coordinate system.
CGSize A pair of CGFloat values that describe the dimensions (width,height) of something.
CGRect The combination of a point (CGPoint) and a size (CGSize) that, together, describe a rectangular area.
Frame and Bounds
View objects have two rectangle (CGRect) properties: bounds and frame. The bounds property describes the coordinate system of the object. All of the view’s graphic content, which includes any subviews, uses this coordinatesystem. The really important thing to understand is that all drawing of a view’s content is performed by that view, and it’s done using the view’s coordinate system—often referred to as its local coordinates.
Moving the view around (in its superview) does not change the view’s coordinate system. All of the graphics within the view remain the same, relative to the origin (upper-left corner) of that view object. In Figure 11-1, the subview is 160 pixels wide by 50 pixels high. Its bounds rectangle is, therefore, ((0,0),(160,50)); it has an origin (x,y) of (0,0) and a size (width,height) of (160,50). When the subview draws itself, it draws within the confines of that rectangle.
The frame property describes the view in the coordinates of its superview. In other words, the frame is the location of a subview in another view—often called its superview coordinates. In Figure 11-1, the origin of the subview is(20,60). The size of the view is (160,50), so its frame is ((20,60),(160,50)). If the view were moved down 10 pixels, its frame would become ((20,70),(160,50)). Everything drawn by the view would move down 10 pixels, but it wouldn’tchange the bounds of the view or the relative coordinates of what’s drawn inside the view.
The size of the bounds and frame are linked. Changing the size of the frame changes the size of its bounds, and vice versa. If the frame of the subview in Figure 11-1 was made 60 pixels narrower, its frame would become((20,60),(100,50)). This change would alter its bounds so it was now ((0,0),(100,50)). Similarly, if the bounds were changed from ((0,0),(160,50)) to ((0,0),(100,40)), the frame would automatically change to ((20,60),(100,40)).
UIView also provides a synthetic center property. This property returns the center point of the view’s frame rectangle. Technically, center is always equal to (CGRectGetMidX(frame),CGRectGetMidY (frame)). If you change the centerproperty, the view’s frame will be moved so it is centered over that point. The center property makes it easy to both move and center subviews, without resizing them.
Converting Between Coordinate Systems
It will probably take you a while—it took me a long time—to wrap your head around the different coordinate systems and learn when to use bounds, when to use frame, and when to translate between them. Here are thequick-and-dirty rules to remember:
n The bounds are a view’s inner coordinates: the coordinates of everything inside that view.
n The frame is a view’s outer coordinates: the position of that view in its superview.
Should you need them, there are a number of methods that translate between the coordinate systems of views. The four most common are the UIView methods listed in Table 11-2. As an example, let’s say you have the coordinates ofthe lower-right corner of the subview in Figure 11-1 in its local coordinates, (160,50). If you want to know the coordinate of that same point in the superview’s coordinate system, send the message [superview convertPoint:CGPointMake(160,50) fromView:subview]. That statement will return the point (180,110), the same point, but in the superview’s coordinate system.
Table 11-2. Coordinate translation methods in UIView
UIView
method Description
-convertPoint:toView: Converts a point in the receiver’s local coordinate system to the same point in the local coordinates of another view.
-convertPoint:fromView: Converts a point in another view’s coordinates into the receiver’s local coordinate system.
-convertRect:toView: Converts a rectangle in the receiver’s local coordinate system to the same rectangle in the local coordinates of another
view.
-convertRect:fromView: Converts a point in another view’s coordinates into the receiver’s local coordinate system.
Also, all of the event-related classes that deliver coordinates report them in the coordinate system of a specific view. For example, the UITouch class doesn’t have a location property. Instead, it has a -locationInView: methodthat translates the touch point into the local coordinates of the view you’re working with.
When Views Are Drawn
In Chapter 4, you learned that iOS apps are event-driven programs. Refreshing the user interface (programmer speak for drawing stuff on the screen) is also triggered by the event loop. When a view has something to draw, itdoesn’t just draw it. Instead, it remembers what it wants to draw and then it requests a draw event message. When your app’s event loop decides that it’s time to update the display, it sends user interface update messages toall the views that need to be redrawn. A view’s drawing lifecycle, therefore, repeats this pattern:
1. Change the data to draw.
2. Send your view object a -setNeedsDisplay message. This marks the view as needing to be redrawn.
3. When the event loop is ready to update the display, your view will receive a-drawRect: message.
You rarely need to send another view a -setNeedsDisplay message. Most views send themselves that message whenever they change in a way that
would require them to redraw themselves. For example, when you set the textproperty of a UILabel object,
the label object sends itself -setNeedsDisplay so the new label will appear. Similarly, a view automatically receives -setNeedsDisplay if it’s changed in a way that would require it to redraw itself, such asadding it to a new
superview. That doesn’t mean that every change to a view will trigger another -drawRect: message. When a view draws itself, the resulting image is saved, or cached, by iOS—like taking a snapshot. Changes that don’t affect that image, such as moving the view around the screen (without resizing it), won’t result in another -drawRect: message; iOS simply reuses the snapshot of the view it already has.
Drawing a View
When your view object receives a -drawRect: message, it must draw itself. In simple terms, iOS prepares a “canvas” which your view object must then “paint.” The resulting masterpiece is then used by iOS to represent your view on the screen—until it needs to be drawn again.
Your “canvas” is a Core Graphics Context, also called your current context, or just context for short.
It isn’t an object, per say.
It’s a drawing environment, which is prepared before your object receives the -drawRect: message.While your -drawRect: method is executing, your code can use any of the Core Graphics drawing routines to “paint” into the prepared context. The context is valid until your -drawRect: method returns, and then it goes away.
For most of the object-oriented drawing methods, the current context is implied. That is, you perform some painting ([myShape fill]) and the -fill method draws into the current context. If you use any of the C drawing functions,you’ll need to get the current context reference and pass that as the call’s first parameter, like this:
CGContextRef currentContextRef = UIGraphicsGetCurrentContext(); CGContextSetAlpha(currentContextRef,0.5f);
A lot of the details of drawing are implied by the state of the current context. The graphics context state
is all of the settings and properties that will be used for drawing in that context. This includes things like the color used to fill shapes, the color of lines, the width of lines, the blend mode, and so on.
Rather than specify all of these variables for every action, like drawing a line, you set up the state for each of the individual properties first. Let’s say you want to draw a shape (myShape), filling it with
the red color and drawing the outline of the shape with the color black:
[redColor setFill]; [blackColor setStroke]; [myShape fill];
[myShape stroke];
The -fill message uses the context’s current fill color, and -stroke uses the current stroke color. This arrangement makes it very efficient to draw multiple shapes or effects using the same, or similar, parameters.
Now the only question remaining is what tools you have to draw with. Your fundamental painting tools are:
n Simple fill and stroke
n Bézier path (fill and stroke)
n Images
That doesn’t sound like a lot, but taken together, they are remarkably flexible. Let’s start with the simplest, the fill functions.
Fill and Stroke Functions
The Core Graphics framework includes a handful of functions that fill a region of the context with
a color. The two principal functions are CGContextFillRect and CGContextFillEllipseInRect. The former fills a rectangle with the current fill color. The latter fills an oval that just fits inside the given rectangle (which will be a circle ifthe rectangle is a square).
CGContextFillRect is often used to fill in the background of the entire view before drawing its details.
It’s not uncommon for a -drawRect: method to begin something like this:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext(); [self.backgroundColor setFill]; CGContextFillRect(context,rect);
This code starts by getting the current graphics context (which you’ll need for the CGContextFillRect call). It then obtains the background color for this view (self.backgroundColor) and makes that color the current fill color.
It then fills the view with that color. Everything drawn after that will draw over a background painted with backgroundColor.
The functions CGContextStrokeRect and CGContextStrokeEllipseInRect perform a similar function, but instead of filling the area inside the rectangle or oval, it draws a line over the outline of the rectangle or oval, using the current linecolor, line width, and line joint style. Stroke is the term used to describe the act of drawing a line.
Images
An image is a picture, and doesn’t need much explaining. You’ve been using image (UIImage) objects since the second chapter. Up until now, you’ve been assigning them to UIImageView objects (and other controls) that drew theimage for you. But UIImage objects are easy to draw into the context of your own view too. The two most commonly used UIImage drawing methods are -drawAtPoint:
and -drawInRect:. The first draws an image into your context, at its original size, with its origin
(upper-left corner) at the given coordinate.
The second method draws the image into the given rectangle, scaling and stretching the image as necessary.
When I say an image is “drawn” into your graphics context, I really mean it’s copied. An image is a two-dimensional array of pixels, and the canvas of your graphics context is a two-dimensional array of pixels. So really,“drawing” a picture amounts to little more than overwriting a portion of your view’s pixels with the pixels in the image. The exceptions to this are images that have transparent pixels or if you’re using atypical blend modes, bothof which I’ll touch on later.
I’ll explain a lot about creating, converting, and drawing images in your custom view later in this chapter by revisiting an app you already wrote. But before I get to that, let’s draw some Bézier paths.
Shapely
You’re going to create an app that uses Bézier paths to draw simple shapes in a custom view.
Through a few iterations of the app, you’ll expand it to include movement and resizing gestures,
and learn about transforms and animation—along with a heap of UIView and Bézier path goodness.
The design of the app is simple, as shown in Figure 11-3.
Figure 11-3. Shapely app design
The app will have a row of buttons that create a new shape. Shapes appear in the middle area where they can be moved around, resized, and reordered. Get started by creating a new project. In Xcode:
n Create a new project based on the single view app template.
n Name the project Shapely.
n Use a class prefix of SY.
n Set the devices to Universal.
The next thing to do is to create your custom view class. You’ve done this several times already. Select the Shapely group in your project navigator and choose New File... (from the File menu or by right/control+clicking on thegroup) and then:
n From the iOS group, choose the Objective-C class template.
n Name the class SYShapeView.
n Make is a subclass of UIView.
n Add it to your project.
Creating Views Programmatically
In this app, you’ll be creating your view objects programmatically, instead of using Interface Builder. In fact, you’ll be creating just about everything programmatically. By the end of the chapter, you should be good at it.
When you create any object, you must begin by initializing it. This is accomplished by sending a new instance an “init” message. Some classes, like NSString, provide a variety of init methods so you can initialize them in different ways: -initWithString:, -initWithFormat:, -initWithData:,
-initWithCharacters:length:, and so on.
The UIView class, however, has what is called a designated initializer. There is only one init message that you should send a new UIView object to prepare it for use, and that message is -initWithFrame:. If you initialize it using anyother init message, it might not work property—so don’t do that. Your subclass is free to define its own init methods, but it must send [super initWithFrame:] so the UIView class gets set up correctly.
Your init method is going to create a new SYShapeView object that will draw a specific shape (square, circle, and so on) with a predetermined frame size. So you’ll need a custom init method that tells
the new object what kind of shape to draw. Your view will draw its shape in a specific color, so you’ll need a property for its color too. Start by editing the SYShapeView.h interface file. Change it so it looks like this (new code in bold):
typedef enum { kSquareShape = 1, kRectangleShape, kCircleShape, kOvalShape, kTriangleShape, kStarShape,
} ShapeSelector;
@interface SYShapeView : UIView
- (id)initWithShape:(ShapeSelector)theShape;
@property (strong,nonatomic) UIColor *color;
@end
The enum statement creates an enumeration. An enumeration is a sequence of constant integer values assigned to names. You list the names and the compiler assigns each a number. Normally the numbers start with zero,but for this app you want them to start at 1 (kSquareShape=1, kRectangleShape=2, kCircleShape=3, and so on). The view will use these values to know which shape to draw.
The -initWithShape: method will be this class’s initializer. It will create the object and establish which shape it will draw. Finally, a UIColor object property will determine the color of the shape. That was painless. Move over to theSYShapeView.m implementation file and write the init method.
Start by deleting the -initWithFrame: method that’s included in the file template. You’re defining your own init method, and won’t be using the default one. Immediately before the @implementation SYShapeView section, add thiscode:
#define kInitialDimension 100.0f
#define kInitialAlternateHeight (kInitialDimension/2)
#define kStrokeWidth 8.0f
@interface SYShapeView ()
{
ShapeSelector shape;
}
@property (readonly,nonatomic) UIBezierPath *path;
@end
The #define statements establish three constants: the initial dimensions (height and width) of most new shape views, an alternate height for shapes that don’t fit in a square, and the thickness of the line used to draw the shape.
Next, you add a private interface section where you define a ShapeSelector instance variable. This variable will determine which shape this view draws. The readonly path property will return a Bézier path object containing thatshape, ready to draw.
The first method to write is your init method. In the @implementation SYShapeView section, add this code:
- (id)initWithShape:(ShapeSelector)theShape
{
CGRect initRect = CGRectMake(0,0,kInitialDimension,kInitialDimension); if (theShape==kRectangleShape || theShape==kOvalShape)
initRect.size.height = kInitialAlternateHeight;
self = [super initWithFrame:initRect]; if (self!=nil)
{
shape = theShape; self.opaque = NO; self.backgroundColor = nil;
self.clearsContextBeforeDrawing = YES;
}
return self;
}
The method begins by defining a default frame with an origin of (0,0) that is kInitialDimension wide and high. It then examines the theShape parameter. If the shape being created is a rectangle or oval, it changes the height of theframe to kInitialAlternateHeight. The only difference between
a square and a rectangle is the aspect ratio of the view. For rectangles and ovals, this changes the aspect ratio so it is no longer 1:1.
Now your method has enough information to send the superclass -initWithFrame:. If the superclass was successfully initialized, then your object can now initialize itself. The first order of business is to remember what kind of shapethis view draws. Next, it alters a few of the UIView’s standard properties.
The most important is resetting the opaque property. If your view object will have transparent regions, you
must declare that your view isn’t opaque. I’ll explain the clearsContextBeforeDrawing property shortly.
The -drawRect: Method
I think it’s time to write your -drawRect: method. This is the heart of any custom view class. Add this method to your SYShapeView.m implementation file (replacing any -drawRect: method supplied by the file template):
- (void)drawRect:(CGRect)rect
{
UIBezierPath *path = self.path; [self.color setStroke];
[path stroke];
}
Whoa! That’s it? Yes, that’s all the code your class needs to draw its shape. It gets the Bézier path object from its path property. The Bézier path defines the outline of the shape this view will draw. You then set the color youwant to draw with, and stroke (draw the outline of) the shape. The details
of how the line is drawn—its width, the shape of joints, and so on—are properties of the path object.
You’ll also notice that you didn’t have to first clear the context (as I explained back in the “Fill and Stroke” section). That’s because you set the view’s clearsContextBeforeDrawing property. Set this to YES and iOS will pre-fill your context with(black) transparent pixels before it sends the -drawRect: message. For views that need to start with a transparent “canvas”—as this one does—why not let iOS do that work for you?
If your view always fills its context with an image or color, set clearsContextBeforeDrawing to NO; leaving it YES will pointlessly fill the context twice, slowing down your app and wasting CPU resources.


No comments:
Post a Comment