Wednesday, 7 May 2014

Declaring Placeholders [If You Build It ...]

Starting with the finished Shapely project from Chapter 11, add a new Objective-C class file, name it SYShapeFactory, and make it a subclass of NSObject. Add another file, but this time choose the
View file template from the iOS  User  Interface group, as shown in Figure 15-8.
If Xcode asks for a device family, pick any one; it wont matter. Name the file SquareShape. This will add a 
standalone Interface Builder (SquareShape.xib) file that creates asingle UIView object to your project.


Figure 15-8. Adding a new Interface Builder file

The SYShapeFactory class will be this files owner. This is your first placeholder object. To use the owner
object, select the new SquareShape.xib file in the navigator, select the File's Owner in the placeholder group,and use theidentity inspector to change its class to SYShapeFactory. You can now connect objects to theSYShapeFactory object that you’ll provide later.

You also need to connect objects—specifically, the gesture recognizers—to your view controller.
To accomplish that, you’ll need a second placeholder object. From the object library, locate the External Objectobject and drag itinto the outline, as shown on the left in Figure 15-9 Select it and change its class to SYViewController. With the placeholder object still selected, use the attributes inspector to assign it an identifier of viewController, as shown on the right in Figure 15-9.
The objects in your new Interface Builder file can now connect to either the SYShapeFactory or your SYViewController object. Now its time to design your objects.


Figure 15-9. Defining a second placeholder

Designing SYShapeView

Select the single view object in the SquareShape.xib file and, using the identity inspector, change its class to SYShapeView.

Switch to the attributes inspector. Xcode doesnt really know what you’re going to use the objects in an Interface Builder file for. By default, it assumes that a top-level view object will become the root view of an interface, so itsizes the view as if it were an iPhone or iPad screen and adds a simulated status bar. For SYShapeView, that isnt the case, so turn all of these assumptions off. Change the simulated size to Freeform  and status bar to None, asshown in Figure 15-10. Now use the attribute and size inspectors to set the following properties:

Set the background to Default (none) Unchek the Opaque property
Make sure Clears  Graphics Context is checked Set its size to 100 by 100


Figure 15-10. Designing the top-level view object

You’ve now replicated the size and properties of a new SYShapeView object produced with the
-initWithShape: method—except for the shape property, which you’ll address in a moment.

Select the SYShapeView.h file, remove the -initWithShape: method prototype, and replace it with a new property:

@property  (nonatomic) ShapeSelector shape;

This makes the shape property settable. We’ll need that later, because we can no longer use -initWithShape: to create the object (Interface Builder will create the object using
-initWithCoder: instead).

Switch to the SYShapeView.m file and make the following changes:

Discard the definitions for kInitialDimension and kInitialAlternateHeight

Remove the shape instance variable from the private @interface SYShapeView  () directive

Delete the entire -initWithShape: method

Replace the one reference to the shape variable with _shape  (in the-path method, just follow the compiler warnings).

See how much code you’ve already eliminated? The entire purpose of the -initWithShape: constructor method was to create and configure a new SYShapeView object. Most of that
work is now being done in your new Interface Builder file.

Connecting the Gesture Recognizers

Back in the SquareShape.xib file, its time to add the gesture recognizers. From the object library, drag out a Pan
Gesture Recognizer and drop it into the SYShapeView object. Select the recognizer object anuse the attributes inspector toset itminimum and maximum touches to 1, as showin Figure 15-11.


Figure 15-11. Creating and configuring the pan gesture recognizer

Switch to the connections inspector and connect its sent action to the -moveShape: method in the view controller placeholder, as shown in Figure 15-12.


Figure 15-12. Connecting the pan gesture recognizer action

You’ve now created a pan gesture recognizer that recognizes only single-finger drag gestures.
Its attached to the shape view object and it sends a -moveShape: message to the view controller, when triggered. The resulting gesture recognizer object is identical to the one you created, configured, and connected in the -addShape: method of SYViewController.

Add the other three gesture recognizers:

1.       Drop a Pinch  Gesture  Recognizer into the shape view.

a.        Connect its sent action to the view controllers -resizeShape: method.

2.       Drop a Tap Gesture  Recognizer into the shape view.

a.        Set its Taps to 2

b.        Set its Touches to 1

c.        Connect its sent action to the -changeColor:  method.

3.       Drop a Tap Gesture  Recognizer into the shape view.

a.        Set its Taps to 3

b.        Set its Touches to 1

c.        Connect its sent action to the -sendShapeToBack: method.

Much of the code you wrote in the -addShape: method has now been replicated using Interface Builder. There are two steps that cant be accomplished in Interface Builder; you’ll address those in code shortly.

Build Your Shape Factory

Select the SYShapeFactory.h file. Add the following #include, @property, and method prototypes
(new code in bold):

#import  "SYShapeView.h"
#import  "SYViewController.h"

@interface SYShapeFactory  : NSObject

@property (strong,nonatomic) IBOutlet  SYShapeView                         *shapeView;
@property (strong,nonatomic) IBOutlet  UITapGestureRecognizer  *dblTapGesture;
@property (strong,nonatomic) IBOutlet  UITapGestureRecognizer  *trplTapGesture;

-  (SYShapeView*)loadShape:(ShapeSelector)shape forViewController:(SYViewController*)controller;

@end

Your shape factory object defines outlets that will be connected to the shape view and selected gesture recognizers. You’ve also declared a -loadShape:forViewController: method that will do all of the work.

This is enough code to complete the necessary connections. Select the SquareShape.xib file, select the File's Owner, and use the connections inspector to connect the shapeView,dblTapGestureand trplTapGesture outlets to their respective objects, as shown in Figure 15-13. Save the file.
(Seriously, save the file by choosing File  Save; its important.)


Figure 15-13. Connecting the factory outlets

The one aspect sorry for the bad pun that has not been addressed is the difference between the square, rectangle, circle, and oval shapes. If you remember-initWithShape: would produce a 100 by 50 pixel view for rectangle and oval shapes and a 100 by 100 pixel view for everything else.
In this version, you’re going to replicate that logic using two Interface Builder files. SYShapeFactory will
choose which one to load.

Start by creating the second Interface Builder file. Select the SquareShape.xib file and choose the
Edit  Duplicate ... command, as shown in Figure 15-14.


Figure 15-14. Creating the RectangleShape.xib file

Name the file RectangleShape. Select the new file, select the shape view object, and use the size inspector to change the height of the shape view to 50, as shown in Figure 15-14. Now you have two Interface Builder files, onethat produces a 100 by 100 view and a second one that creates a 100 by 50 view.

Now switch to the SYShapeFactory.m file. Add a class method that will choose which Interface Builder file (SquareShape or RectangleShape) to load for given shape (new code in bold):

#import  "SYShapeFactory.h"

@interface   SYShapeFactory ()
+  (NSString*)nibNameForShape:(ShapeSelector)shape;
@end

@implementation   SYShapeFactory

+  (NSString*)nibNameForShape:(ShapeSelector)shape
{
switch   (shape) {
case  kRectangleShape: case  kOvalShape:
return  @"RectangleShape";

default:
return  @"SquareShape";
}
}

Loading an Interface Builder File

You’re now ready to create your shape view and gesture recognizer objects by loading an Interface Builder file. Write the -loadShape:forViewController: method now:

-  (SYShapeView*)loadShape:(ShapeSelector)shape forViewController:(SYViewController*)controller;
{
NSDictionary *placeholders  = @{  @"viewController": controller }; NSDictionary *options  = @{  UINibExternalObjects: placeholders  };
[[NSBundle  mainBundle]   loadNibNamed:[SYShapeFactory nibNameForShape:shape] owner:self
options:options]; self.shapeView.shape = shape;
[_dblTapGesture requireGestureRecognizerToFail:_trplTapGesture];

return  _shapeView;
}

The first two statements prepare the view controller to be a placeholder object when the Interface Builder file is loaded. You may pass as many placeholder objects as you like, just make sure their classes and identifiersagree with the external objects you defined in the Interface Builder file.

The third statement is where the magic happens. The loadNibNamed:owner:options: method searches
your apps bundle for an Interface Builder file with that name. The name (SquareShape or RectangleShape
is determined bythe +nibNameForShape: method you added earlier. The owner parameter becomes the files owner placeholder object. The options parameter is a dictionary of special options. In this case, the
only special option is additional placeholder objects (UINibExternalObjects).

When -loadNibNamed:owner:options: is sent, the owner and any additional placeholder objects take the place of the File's Owner and the corresponding external objects defined in the Interface Builder file. The objects in the fileare created, the properties of the objects are set according to the
attributes you edited, and finally all of the outlet and action connections are established.

The message returns an NSArray containing all of the top-level objects created in the file.
You can access the objects created by the file either through this array or via outlets that you
connected to the placeholders. In thisapp, you’ve used the latter technique.

The last two statements take care of the two steps that cant be accomplished in Interface Builder.
The shape property of the view is set and the double-tap/triple-tap dependency is established.

Replacing Code

Switch to the SYViewController.m file. Add an #import  "SYShapeFactory.h" statement after the other #import statements. Now find the -addShape: method and replace the code that programmatically created a newSYShapeView object with the following (modified code in bold):

-  (IBAction)addShape:(id)sender
{
SYShapeView  *shapeView = [[SYShapeFactory  new] loadShape:[sender tag] forViewController:self];

Now, for the fun part: find the code in -addShape: that creates, configures, and connects the four gesture recognizers and delete it all. You dont need any of that now. All four of the gesturrecognizers were created,configured, and connected by the Interface Builder file.

Run the finished app and observe the results. You shouldnt be able to tell any difference between this version of Shapely and the one from Chapter 11, which is the point. This exercise underscores the major advantages anddisadvantages of creating your objects in Interface Builder:

n      Objects are easy to create, configure, and connect in Interface Builder. This reduces the amount of code you have to write, saving time, and potentially reducing bugs. (Advantage)

n     There are some properties and object relationships—like the double-tap/triple-tadependency that cannot be set in Interface Builder, and must be performed programmatically. (Disadvantage)

n     You can easily choose between multiple Interface Builder files.Instead of writing huge if/else or switch 
     statements, you can create a completely different set of objects simply by selecting a
different Interface Builder file. (Advantage)

n     You’re limited to the configuration and initialization methods supported by Interface Builder. In Shapely, you had to prove a settable shape property in order to “fix” the object after it was created, since youcould no longer use the
-initWithShape: method. (Disadvantage)

n     Interface Builder makes it easy to create complex sets of objects, especially ones like gesture recognizers and layout constraints. It requires pages and pages of dense, difficult to read, code toreproduce the layout constraints required for many interfaces. (Big Advantage) 
n     It can sometime take considerable effort to obtain references to the objects created
in an Interface Builder file.You might have to create special placeholder objects, or tediously dig through the top-levelobjects returned by -loadNibNamed:owner:options:. In this section you created a class
(SYShapeFactory) for the sole purpose of providing outlets for the references to
the shape view and gesture recognizers. (Disadvantage) 

Interface Builder files arent the best solution for every interfacesometimes a few lines of well-written code are all you need. But in the majority of cases, Interface Builder can save you from writing, maintaining, and debugging(literally) thousands of lines of code. Its an amazingly flexibland efficientool that can free you from hours of work and improve the quality of your apps.
You just needed to know how it works and how to use it.

Summary

Interface Builder is one of the cornerstones of Xcode, and its what makes iOS app development so smooth. Understanding how it works gives you an edge. Understand what it can do and how, and you can push it to its limitsor take over with your own code; its your choice.

Loading Interface Builder files directly is where the real flexibility of Interface Builder becomes evident. You now know how to define practically any interface, a fragment of an interface, or just some arbitrary objects in anInterface Builder file and load them when, and where, you want.
You know how to create any kind of objects you like, set its custom properties, and connect those with existing objects in your app. Thats an incredibly usefultool to have at your fingertips.

No comments:

Post a Comment