Tuesday, 6 May 2014

Creating a Custom View Class [Draw Me a Picture]

You create a custom view by subclassing either UIView or UIControl, depending on whether your intent is to create a display object or something that acts like a control, like a new kind of switch. In this chapter you’ll only besubclassing UIView.

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

Lets start at the top—literally.


View Coordinates

The devices 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-dowfrom the Cartesian coordinate system you learned in school, or maybe from reading geometry bookin your spare time. Forcomputer programs, this arrangement is morconvenient; most content “flows from the upper-left corner, so its usually simpler to perform calculations from the upper-lefcorner 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 views graphic content, which includes any subviews, uses this coordinatesystem. The really important thing to understand is that all drawing of a views content is performed by that view, and its done using the views coordinate system—often referred to as its local coordinates.

Moving the view around (in its superview) does not change the views 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, thorigin of the subview is(20,60). The size of the view is (160,50), sits 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 wouldntchange the bounds of the view or the relative coordinates of whats 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 viewframe rectangle. Technically, center is always equal to (CGRectGetMidX(frame),CGRectGetMidY (frame)). If you change the centerproperty, the views frame will bmoved sit 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 views inner coordinates: the coordinates of everything inside that view.

n     The frame is a views outer coordinates: the position of that view in its superview.

Shoulyou need themthere are a numbeof methods that translate betweethe coordinate systems of views. The four most common are the UIView methods listed in Table 11-2. As an example, lets say you have the coordinates ofthe lower-right corner of the subview in Figure 11-1 in its locacoordinates(160,50). If you want to know the coordinate of that same point in the superviews coordinate system, send the message [superview convertPoint:CGPointMake(160,50) fromView:subview]. That statemenwilreturn the point (180,110), the same point, but in the superviews coordinate system.

Table 11-2. Coordinate translation methods in UIView

UIView method                               Description

-convertPoint:toView:                       Converts a point in the receivers local coordinate system to the same point in the local coordinates of another view.
-convertPoint:fromView:                  Converts a point in another views coordinates into the receivers local coordinate system.
-convertRect:toView:                        Converts a rectangle in the receivers local coordinate system to the same rectangle in the local coordinates of another view.
-convertRect:fromView:                   Converts a point in another views coordinates into the receivers 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 doesnt 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, itdoesnt just draw it. Instead, it remembers what it wants to draw and then it requests a draw event message. When your apps event loop decides that its time to update the display, it sends user interface update messages toall the views that need to be redrawn. A views 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 senthemselves thamessage whenever they change in a way that 
would require them to redraw themselves. For examplewhen 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 its changed in a way that would require it to redraw itself, such asadding it to a new 
superviewThat doesnt 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 dont affect that image, such as moving the view around the screen (without resizing it), wont 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 isnt an object, per say.

Its 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 perforsome 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 callfirst parameter, like this:

CGContextRef  currentContextRef = UIGraphicsGetCurrentContext(); CGContextSetAlpha(currentContextRef,0.5f);

A lot of the detailof drawing are implied bthe state of the current context. The graphics context state
is all of the settings and properties that wilbe used for drawing in that context. This includes thinglike 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. Lets 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 contexts 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 doesnt sound like a lot, but taken together, they are remarkably flexible. Lets 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.
Its 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 doesnt 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 drawthe image into the given rectanglescaling and stretching the image as necessary.

When I say an image is “drawn” into your graphics context, I really mean its 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 views 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, lets 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 dont 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=1kRectangleShape=2, kCircleShape=3, and so on). The view will use these values to know which shape to draw.

The -initWithShape: method will bthis classs 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 thats included in the file template. You’re defining your own init method, and wont 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 dont 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 superclaswas successfully initialized, then your object can now initialize itself. The first order of business is tremember what kind of shapethis view draws. Next, it alters a few of the UIViews standard properties.

The most important is resetting the opaque property. If your view object will have transparent regions, you 
must declare that your view isnt opaque. I’ll explain the clearsContextBeforeDrawing property shortly.

The -drawRect: Method

I think its 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! Thats it? Yes, thats 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 drawYou 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 didnt have to first clear the context (as I explained back in the “Fill and Stroke” section). Thats because you set the views clearsContextBeforeDrawing property. Set this to YES and iOwill pre-fill your context with(black) transparent pixels before it sends the -drawRect: message. For views that need to start with a transparen“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