Tuesday, 6 May 2014

Creating the Bézier Path [Draw Me a Picture]

Clearly, the heavy lifting is creating that Bézier path object. Do that now. Add this method to your
@implementation:

-  (UIBezierPath*)path
{
CGRect bounds  = self.bounds;
CGRect rect = CGRectInset(bounds,kStrokeWidth/2+1,kStrokeWidth/2+1);

UIBezierPath *path; switch (shape)  {
case   kSquareShape: case   kRectangleShape:
path   = [UIBezierPath bezierPathWithRect:rect]; break;

default:
// TODO:  add  cases for  remaining shapes break;
}
path.lineWidth = kStrokeWidth; path.lineJoinStyle = kCGLineJoinRound; return path;
}

This method implements the getter for your objects path  property. Its job is to return a UIBezierPath object that describes the shape this view draws (square, rectangle, circle, and so on), exactly fitting its current size (bounds).

The first two lines of code create a CGRect variable that describes the outer dimensions of the shape. The reason it is kStrokeWidth/2+1 pixels smaller than the bounds is explained in the Avoiding Pixelitis” sidebar.

All coordinates in Core Graphics are mathematical points in space; they do not address individual pixels. This is an important concept to understand. Think of coordinates as infinitely thin lines between the pixels of yourdisplay or image. This has three ramifications:

       Points or coordinates are not pixels.

       Drawing occurs on and inside lines, not on or inside pixels.

       One point may not mean one pixel.

When you fill a shape, you’re filling the pixels inside the infinitely thin lines that define that shape.
In the following figure, a rectangle ((2,1),(5,2)) is filled with a dark color. A lower-resolution display will have one physical pixel per coordinate space, as shown on the left. On the right is a “retina” display, with four physical pixels per coordinatespace.


The rectangle defines a mathematically precise region, and the pixels that fall inside that region are filled 
with the color. This precision avoids a common programmer malady known as pixelitis: the anxiety of not knowingexactly what pixels will be affected by a particular drawing operation, common in many other graphic libraries.

This mathematical  precision can have unanticipated  side effects. One common artifact occurs when
drawing a line with an odd width—“odd” meaning “not evenly divisible by 2.” A lines stroke is centered over themathematical line or curve. In the next figure, a horizontal line segment is drawn between two coordinates, with a stroke width of 1.0. The upper line in the next figure does not draw a solid line on a lower-resolution display,because the stroke only covers ½ of the pixels on either side of the line. Core Graphics draws partial pixels using anti-aliasing, which means that the color of those
pixels is adjusted using half the strokes color value. On a retina display, this doesn’t occur because each pixel is ½ of a coordinate value.


The lower line in the figure avoids the “half-pixel” problem by centering the line between two coordinates. 
Now the 1.0 width line exactly fills the space between coordinate boundaries, neatly filling the pixels, and appearing to the user as a clean, solid line.

If pixel-prefect alignment is important to your app, you may need to consult the contentScaleFactor property of
UIView. It discloses the number of physical screen pixels between two whole coordinate values. As of this writing, it can be one of two values: 1.0 for lower resolution displays and 2.0 for retina displays.

The next block of code creates a UIBezierPath variable and then switches on the shape variable
to build the desired shape. For right now, the case statement only makes the paths for square and rectangular shapes, as shown in Figure 11-4. You’ll fill in the other cases later.


Figure 11-4. Unfinished -path method

Sharp-eyed readers will notice that the code to create a square shape and a rectangular shape are the same. Thats because the difference between these shapes is the aspect ratio of the view, and that was established in -initWithShape: when the object was created. If you go back and look at
-initWithShape: you’ll see these two lines of code:

if (theShape==kRectangleShape || theShape==kOvalShape) initRect.size.height = kInitialAlternateHeight;

When the views frame was initialized, it was made half as high if the shape was a rectangle or oval.
All other shape views begin life with a square frame.

Finally, the line width of the shape is set to kStrokeWidtand the joint style is set to kCGLineJoinRound.
This lastproperty determinehow a joint (the point where one line segment ends and the next beginsis drawn. Setting it tokCGLineJoinRound draws shapes with rounded “elbows.”

Testing Squares

Thats enough code to draw a square-shaped view, so lets hook this up to something and try it out. The Shapely app creates new shapes when the user taps a button, so define a button to test it. The buttons get customimages, so start by adding those image resources to your project. Select the Images.xcassets asset catalog item in the navigator. Find the Learn  iOS Development  Projects ➤ Ch 11  Shapely   (Resources) folder and drag all 12 of the image files (addcircle.pngaddcircle@2x.png, addoval.png, addoval@2x.png, addrect.png, addrect@2x.png addsquare.pngaddsquare@2x.png, addstar.png, addstar@2x.png, addtriangle.png, and addtriangle@2x.png) into theasset catalog, as shown in Figure 11-5. There are also some app icons in the Shapely   (Icons) folder, which you’re free to drop into the AppIcon group.


Figure 11-5. Adding button image resources

I’m going to start with the iPad interface this time and copy the finished work into the iPhone interface later. If you have an iPhone/iPod and want to play with this app on your device as you develop it, go ahead and startwith the iPhone interface instead—the steps are the same.

Select the Main_iPad.storyboard (or _iPhone.xib) file. Switch to the assistant view (View  Assistant Editor  Show Assistant Editor).
The interface for the interface View Controller (SYViewController.hwill appear in the right-handpane.
If it doesnt, select the SYViewController.h file from the navigation ribbon immediately above the right-hand editor pane. Bring up the object library (View  Utilities ➤ Show Object Library) and drag a new Butto object intoyour interface, as shown in Figure 11-6.


Figure 11-6. Adding the first button

Switch to the attributes inspector, select the root view object, and change its background property to
Black  Color. Select the new button again and make the following changes:

n     In the attributes inspector

n     Change its type to Custom

n     Erase its title (replacing “Button”  with nothing)

n     Change its image to addsquare

n     Using the size inspector

n     Change its width and height to 44 pixels

In the SYViewController.h file (in the right-hand editing pane), add a new action:

-  (IBAction)addShape:(id)sender;

Connect the button to the action by dragging the connection socket next to the -addShape:
declaration into the new button, as shown in Figure 11-7.



Figure 11-7. Connecting the first button

Switch to the SYViewController.m implementation file anadd the action method. Begiby adding aimport statement immediately after the others, sthis module knows about the SYShapeView class:


#import "SYShapeView.h"

Towards the end of the @implementation  section, add the new action method:

-  (IBAction)addShape:(id)sender
{
SYShapeView  *shapeView  = [[SYShapeView alloc] initWithShape:kSquareShape]; shapeView.color = [UIColor whiteColor];
[self.view  addSubview:shapeView];

CGRect shapeFrame  = shapeView.frame;
CGRect safeRect = CGRectInset(self.view.bounds, shapeFrame.size.width, shapeFrame.size.height);
CGPoint newLoc = CGPointMake(safeRect.origin.x
+arc4random_uniform(safeRect.size.width), safeRect.origin.y
+arc4random_uniform(safeRect.size.height));
shapeView.center = newLoc;
}

Your shape view is now ready to try out.
The addShape: action creates a new SYShapeView object that draws a square. It assigns it a color of white, and adds it as a new subview in the root view.

Up until this point in this book, you’ve been creating and adding view objects using Interface Builder. This code demonstrates how you do it programmatically. Anything you add to a view using Interface Builder can be createdand added programmatically, and you can create things in code that you cant create in Interface Builder.

The rest of the code in -addShape: just picks a random location for the new view, making sure it isnt too close to the edge of the display. Remember that SYShapeView-initWithShape: method set the frame for the view, but its origin was left at (0,0). Unless you change that, all new shape views would appear in the upper-left corner of the view.

Fire up the iPad simulator and give your app a run, as shown in Figure 11-8.
Tap the button a few times to create some shape view objects, as shown on the right in Figure 11-8.


Figure 11-8. Working square shape views

So far, you’ve designed a custom UIView object that draw a shape using a Bézier path. You’ve createan actionthat creates new view objects and adds theto a view, programmatically. 
This is a great start, but you still want todraw different shapes, in different colors, so expand the app to do that. 

More Shapes, More Colors

Back in Xcode, stop the app and switch to the Main_iPad.storyboard (or _iPhone.xib) file again. Your app wildraw six different shapes, so create five more buttons. I did this by holding down the option keand dragging out copies of thefirst UIButton object, as showin Figure 11-9.
You could, alternativelycopy and paste the first button. If you’re a masochist, you could drain new button
objects from thlibrary and individually change them to match the first one. I’llleave those decisions to you.


Figure 11-9. Duplicating the first button

Just as you did in DrumDub, you’ll use the ta property of the button to identify the shape it wilcreateSince you duplicated the first button, all of the buttons are connected to the same -addShape: action in SYViewController. (Ifnot, connect them now.) Working from left to right, use the attributeinspector to set the ta and image property of the buttons using Table 11-3.

Table 11-3. New shape button properties

                                                     Tag              Image

1                      addsquare

2                      addrect

3                      addcircle

4                      addoval

5                      addtriangle

6                      addstar

You’ll notice that the tag values, cleverly, match up with the enum constants you defined in SYShapeView.h.
For each button to create its shape, change the first line of -addShape: (in SYViewController.m) to usthe buttons tag value instead of the kSquareShape constant:

SYShapeView  *shapeView  = [[SYShapeView alloc] initWithShape:[sender tag]];

Of course, the path  property in SYShapeView still only knows how to create shapes for squares and rectangles, so you’re not done yet. But before you leave SYViewConroller.m, lets make things a little more colorful. In the private@interface  SYViewController () section, add an array instance variable and a readonly colors property:

@interface  SYViewController ()
{
NSArray  *colors;
}
@property (readonly,nonatomic) NSArray  *colors;
@end

In the @implementation  section, add a (lazy) property getter for the colors array:

-  (NSArray*)colors
{
if (colors==nil)
{
colors = @[  UIColor.redColor,UIColor.greenColor, UIColor.blueColor,UIColor.yellowColor, UIColor.purpleColor,UIColor.orangeColor, UIColor.grayColor,UIColor.whiteColor ];
}
return colors;
}

This method creates an array of UIColor objects that will be used to assign different colors to the shapes. It only creates the array once—the first time its received. Now change -addShape: again so it assigns a random color toeach new shape view:

-  (IBAction)addShape:(id)sender
{
SYShapeView  *shapeView  = [[SYShapeView alloc]  initWithShape:[sender tag]]; shapeView.color = [self.colors objectAtIndex:arc4random_uniform(self.colors.count)];

To draw those shapes, your SYShapeView object still needs some work. Switch to the SYShapeView.m
file, find the -path property getter method, and finish it out with the code shown in bold in Listing 11-1Oh, and you might as well remove the default: case from the unfinished version; you dont need thaanymore.

Listing 11-1. Finished path property getter method

-  (UIBezierPath*)path
{
CGRect bounds  = self.bounds;
CGRect rect = CGRectInset(bounds,kStrokeWidth/2+1,kStrokeWidth/2+1);

UIBezierPath *path; switch (shape)  {
case   kSquareShape: case   kRectangleShape:
path   = [UIBezierPath bezierPathWithRect:rect]; break;

case  kCircleShape: case  kOvalShape:
path  = [UIBezierPath bezierPathWithOvalInRect:rect]; break;

case  kTriangleShape:
path  = [UIBezierPath bezierPath];
CGPoint point   = CGPointMake(CGRectGetMidX(rect),CGRectGetMinY(rect)); [path  moveToPoint:point];

point   = CGPointMake(CGRectGetMaxX(rect),CGRectGetMaxY(rect)); [path  addLineToPoint:point];
point   = CGPointMake(CGRectGetMinX(rect),CGRectGetMaxY(rect)); [path  addLineToPoint:point];
[path  closePath]; break;

case  kStarShape:
path  = [UIBezierPath bezierPath];
point   = CGPointMake(CGRectGetMidX(rect),CGRectGetMinY(rect)); float angle  = M_PI*2/5;
float  distance = rect.size.width*0.38f; [path  moveToPoint:point];
for  ( NSUInteger arm=0; arm<5; arm++  )
{
point.x += cosf(angle)*distance; point.y += sinf(angle)*distance; [path  addLineToPoint:point]; angle  -=  M_PI*2/5;
point.x += cosf(angle)*distance; point.y += sinf(angle)*distance; [path  addLineToPoint:point]; angle  += M_PI*4/5;
}
[path  closePath]; break;
}
path.lineWidth = kStrokeWidth; path.lineJoinStyle = kCGLineJoinRound; return path;
}

The kCircleShape and kOvalShape cases use another UIBezierPath convenience method to create a finished path object that traces an ellipse that fits exactly inside the given rectangle.

The kTriangleShapcase is where things get interesting. It shows a Bézier path being created, online segment at a time. You begin a Bézier path by sending it a -moveToPoint: message to establisthe first point in the shape.Aftethat, you add line segments by sending a series ofaddLineToPoint: messages. Each message adds one edge to the shape, just like playing “connect
the dots. The last edge is created using the -closePath: message, which does two things: it connects the last point to the first point,and makes this a 
closed path—one that describes a solid shape.

The kStarCase creates an even more complex shape. If you’re curious about the details, read the comments in the finished Shapely project code that you’ll find in the Learn  iOS Development Projects  Ch 11  Shapely  folder.In brief, the code creates a path that starts at the top-center of the view (the top point of the star), adds a line that angles down to the interior point of the starand then another (horizontal) line back out to the right-hand point of the star. It then rotates 72° and repeats these steps, four more times, to create a five-pointed star.

Run your app again (see Figure 11-10) and make a bunch of shapes!


Figure 11-10. Multicolor shapes

Transforms

Next up on your apps feature list is dragging and resizing shapes. To implement that, you’re going to revisit gesture recognizers, and learn something completely new. Start with the gesture recognizer.

Like view objects, you can create, configure, and connect gesture recognizers programmatically. The concrete gesture recognizer classes supplied by iOS (tap, pinch, rotate, swipe, pan, and long-press) have all the logic needed to recognize these common gestures. All you have to do is instantiate one, do a little configuration, and hook them up.

Return to the -addShape: action method in SYViewController.m. At the end of the -addShape:
method, add this code:

UIPanGestureRecognizer *panRecognizer;
panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(moveShape:)];
panRecognizer.maximumNumberOfTouches = 1; [shapeView  addGestureRecognizer:panRecognizer];

The first three statements create a new pan (drag) gesture recognizer object. This recognizer will send its action message (moveShape:) to your SYViewController object (self). The maximumNumberOfTouches property is 
set to 1. Thisconfigures the object to only recognize single finger drag gestures; it will ignore any two or three fingerdrags that it witnesses. Finally, the recognizer object is attached to the shape view that was just created and 
added to thesuperview.

Now all you need is a -moveShape: action. At the beginning of the SYViewController.m file, locate the private @interface  SYViewController () section and add this method declaration:

-  (IBAction)moveShape:(UIPanGestureRecognizer *)gesture

Scroll down to the end of the @implementation  section and add the method:

-  (IBAction)moveShape:(UIPanGestureRecognizer *)gesture
{
SYShapeView  *shapeView  = (SYShapeView*)gesture.view;
CGPoint dragDelta = [gesture  translationInView:shapeView.superview]; CGAffineTransform  move;

switch (gesture.state) {
case   UIGestureRecognizerStateBegan: case   UIGestureRecognizerStateChanged:
move = CGAffineTransformMakeTranslation(dragDelta.x,dragDelta.y); shapeView.transform = move;
break;

case   UIGestureRecognizerStateEnded: shapeView.transform = CGAffineTransformIdentity;
shapeView.frame = CGRectOffset(shapeView.frame,dragDelta.x,dragDelta.y); break;

default:
shapeView.transform = CGAffineTransformIdentity; break;
}
}

Gesture recognizers analyze and absorb the low-level touch events sent to the view object and turn those into high-level gesture events. Like many high-level events, they have a phase. The phase of continuous gestures, like dragging, progress through a predictable order: possiblebegan, changed, and finally ended or canceled.

Your -moveShape: method starts by getting the view that caused the gesture action; this will be the view the user touched and the one you’re going to move. It then gets some information about how far the user dragged and thegestures state. As long as the gesture is in the “began” or “changed” state, it means the user touched the view and is dragging their finger around the screen. When they release it, the state will change to “ended.” In rarecircumstances, it may change to “cancel” or “failed,” in which case you ignore the gesture.

While the user is dragging their finger around, you want to adjust the origin of the shape view by the same distance on the screen, which gives the illusion that the user is physically dragging the view around the screen. (I hopeyou didnt think you could actually move things inside your iPhone by touching it.) The way you’re going to do that uses a remarkable feature of the UIView class: the transform property.

No comments:

Post a Comment