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 object’s 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 line’s 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 stroke’s 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. That’s 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 view’s 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 kStrokeWidth and the joint style is set to kCGLineJoinRound.
This lastproperty determines how a joint (the point where one line segment ends and the next begins) is drawn. Setting it tokCGLineJoinRound draws shapes with rounded “elbows.”
Testing Squares
That’s enough code to draw a square-shaped view, so let’s 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.png, addcircle@2x.png, addoval.png, addoval@2x.png, addrect.png, addrect@2x.png, addsquare.png, addsquare@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.h) will appear in the right-handpane.
If it doesn’t, 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 Button 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 and add the action method. Begin by adding an import statement immediately after the others, so this 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 can’t create in Interface Builder.
The rest of the code in -addShape: just picks a random location for the new view, making sure it isn’t too close to the edge of the display. Remember that SYShapeView’s -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 created an actionthat creates new view objects and adds them to 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 will draw six different shapes, so create five more buttons. I did this by holding down the option key and dragging out copies of thefirst UIButton object, as shown in Figure 11-9.
You could, alternatively, copy and paste the first button. If you’re a masochist, you could drag in new button
objects from the library 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 tag property of the button to identify the shape it will create. Since 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 attributes inspector to set the tag 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 use the button’s 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, let’s 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 it’s 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-1. Oh, and you might as well remove the default: case from the unfinished version; you don’t need that anymore.
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 kTriangleShape case is where things get interesting. It shows a Bézier path being created, one line segment at a time. You begin a Bézier path by sending it a -moveToPoint: message to establish the first point in the shape.After that, 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 star, and 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 app’s 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: possible, began, 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 thegesture’s 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 didn’t 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