CPSC 91 Spring 2011: Lab 03


Introduction

In this week's lab, you will work on a few extensions to both the Hangman app and the Craps app.

Like last week's lab, your code:

Part 1: Struts and Springs

For the first part, you should begin by downloading the version of Hangman that I demonstrated in class. Your job is to modify the XIB file using the struts and springs on the interface elements (the buttons and labels) so that the app still looks reasonable whether the device is in landscape mode (normal) or portrait mode (rotated).

First, you will need to tell your app that it should allow rotation to any orientation. To do this, simply add the following method to your view controller:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    return YES;
}

If you now rotate your device you will see that the view rotates too. Unfortunately, the view is now all messed up in landscape mode. (Note: You can rotate the simulator by choosing Hardware -> Rotate Left or Rotate Right; alternatively apple-LeftArrow and apple-RightArrow are keyboard shortcuts to do this.) To fix the problem, you will want to modify the struts and springs in interface builder to allow elements to move around based on the rotation of the device. You can preview this rotation by clicking the little circular arrow in the top right of the interface builder window. You are allowed to shift elements up or down as necessary, but you can dramatically alter the existing interface. Also, you can only use interface builder for this part of the assignment. You will programmatically move UI elements in Part 3 of the lab.

Part 2: Custom DiceView

In this part of the lab, you will replace the glyphs that you had been using to display the dice in your craps app with your own custom UIView called DiceView. To do this, I'd recommend you begin from my solution (posted shortly), but you can use your own craps app if you'd like. The reason I think starting from my solution will be easier is that your interface may make Part 3 more difficult. Add a new file to your project of type Objective-C class, making sure to select Subclass of UIView in the middle of the New File dialog box. Name this new view DiceView.

DiceView will do custom drawing in -drawRect: to draw a picture of the dice. The only code you will need in your DiceView.m file is the code in -drawRect: to draw the face of the die.

To get this working you will need to do a few things:

  1. Implement -drawRect: to draw the die face.
  2. Add two DiceView objects to your .xib file and wire them up to your view controller.
  3. Declare a DiceViewDelegate protocol.
  4. Implement the DiceViewDelegate in your view controller.

Drawing the die face

Using the examples you've seen so far, you should be able to complete this, but here are some functions that you'll probably want to make use of. Look them up in the documentation if you can't remember how they work.
UIGraphicsGetCurrentContext();
CGContextBeginPath(CGContextRef c);
CGContextClosePath(CGContextRef c);
CGContextSetLineWidth(CGContextRef c, CGFloat w);
CGContextMoveToPoint(CGContextRef c, CGFloat x, CGFloat y);
CGContextAddLineToPoint(CGContextRef c, CGFloat x, CGFloat y);
CGContextStrokePath(CGContextRef c);
CGContextFillPath(CGContextRef c);
CGContextAddArc(CGContextRef c, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, 
                CGFloat endAngle, int clockwise)

Be sure your dice are drawn to the size of the their bounds, not to some fixed size.

You might be wondering what the value of the die is that you're supposed to be drawing. Your UIView isn't allowed to have a pointer back to your controller, so it can't just look it up. The -drawRect: method can't take the value of the die as a parameter. What we're going to do is set up a protocol in our DiceView.h file that will allow us to ask for the value of the die using delegation.

But before you begin down the road to implementing this delegate, you should make sure your -drawRect: method works properly. Do this by putting a DiceView in your .xib file and hard-coding a value for the face of the die into -drawRect: (trying each value 1-6). Once you're confident that the die is displaying properly, continue on.

Wire up your DiceViews

In the previous step, you put at least one DiceView object in your .xib file. Before continuing, be sure you have two. Create IBOutlets for the two DiceView objects in your view controller and wire them up in interface builder. They won't display the right values yet, but once we get the delegate working, they will. However, we're one step short of that happening. Remember that when you rolled the dice, your view controller had to change the text of the UILabels in the interface. Here, what you need to do is redraw your DiceView object if the dice values change. You should never call -drawRect: directly, though. Instead, if you want to redraw your DiceView object, you call the setNeedsDisplay method on each of your DiceView objects. This tells the device that the objects need to be redrawn, and when it's time for them to be redrawn, your -drawRect: method will be called for you.

DiceViewDelegate declaration

We're going to use a delegate to get the values of the dice. We'll call this delegate DiceViewDelegate. If we want to use a delegate, we need to write a protocol listing the methods we need our delegate to implement for us. Here, we only need our delegate to implement one method:

- (int)dieValueForDiceView:(DiceView*)diceView;
Notice we pass along an instance of ourselves when we call the use the delegate method. This will be very important here because we're going to need to distinguish between the two DiceView objects that are on the screen.

Another requirement for using a delegate is that we need an instance variable to point to the delegate. Following naming conventions, we'll name this instance variable delegate. The type of instance variable is written as id <DiceViewDelegate>. This says that the delegate is a pointer to any object that implements the DiceViewDelegate protocol. Create an (assign) property for the delegate using the same type. Notice that even though this delegate is an object, we do not use retain or copy. Otherwise, your DiceView object will be retaining its delegate (which will be your view controller) and your view controller will be retaining your DiceView (since it's in your view controller's view hierarchy). This is known as a retain cycle, and you'd to avoid this.

Returning to your DiceView.m file, be sure to synthesize your delegate. Also, you can now replace your hard-coded die value with a call to your delegate method.

Implement the DiceViewDelegate

In your view controller, you're going to want to implement the DiceViewDelegate protocol. Recall that you first have to declare that you're implementing the protocol in the .h file, then you have to implement all required and any optional methods in the protocol in the .m file. Our protocol has only one required method, -dieValueForDiceView:. ("Required" is the default if you don't say otherwise in your protocol declaration.) You just need to return the die value corresponding to the correct DiceView that's asking. Remember that the DiceView that's asking for a value passed itself in to the -dieValueForDiceView: method, so you should be able to think of way of figuring out which one it is.

Be sure to call -setNeedsDisplay each time the dice are rolled. Once everything is working properly, remove the old UILabels you had been using from your .xib and replace them with your new DiceViews.

Part 3: Programmatically supporting rotation

First, add this to your view controller to allow your app to rotate (as you did in Part 1).
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    return YES;
}

In Part 1, we used struts and springs to handle rotation. However, sometimes you need to just pick up UI elements and put them somewhere different because the different orientations can't fit the same layout. Use struts and springs for as much of the UI as you can, but either out of necessity (interface builder won't let you do exactly what you want) or out of requirement (it's part of the lab!), be sure that at least one element has no struts or springs set. We'll move that element around programmatically when we detect rotation.

There are two choices for the method you should implement in your view controller:

// this is called just before the view rotates
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
                                duration:(NSTimeInterval)duration


// this is called just after the view rotates
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
Your implementation of this method will have one of the following structures:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
                                duration:(NSTimeInterval)duration
{
    if (UIInterfaceOrientationIsPortrait(toInterfaceOrientation)) {
       //do something if it is about to be rotated from portrait to landscape.
    } else {
       //do something if it is about to be rotated from landscape to portrait.
    }
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
    if (UIInterfaceOrientationIsPortrait(fromInterfaceOrientation)) {
       //do something if it got rotated from landscape to portrait.
    } else {
       //do something if it got rotated from portrait to landscape.
    }
}
What you'll want to do in those "do something" places is either: As you did in your -drawRect method for the DiceView, don't hardcode specific (x,y) coordinates. Instead, try to move UI elements in relation to other UI elements on the screen or in relation to the bounds of your superview (it's a property).