4. Developing Views and View Controllers

In this chapter, we will flesh out the enter weight and history views for our Health Beat application. As we proceed, we will gain more experience graphically laying out views and linking user interface elements to their view controllers. We will also learn how to populate, monitor, and control a table view; how to set up static table views; and the differences between using a navigation controller and simply presenting modal views.

This chapter also includes a discussion of advanced techniques. We use Core Animation to modify a view’s appearance, which lets us create rounded buttons with custom background colors.

Entering Weight Data

Let’s start by modifying our enter weight view. Open the MainStoryboard.storyboard file, and zoom in on the enter weight scene.

Drag a label from the library and drop it anywhere in the view. Double-click the label, and change the text to Enter Today’s Weight. In the Attributes inspector, change the font to System Bold 24.0. You can tap the image icon to bring up the Fonts window. Next, choose Editor > Size to Fit Content from the menu bar. Finally, center the label along the top of the view using the guidelines (Figure 4.1).

Figure 4.1 Centering the label

image

Drag out a second label and position it below the first. Change its text to Current Date and Time. Don’t worry, that’s just a placeholder. We will replace this text at runtime. Of course, we don’t know exactly how long the date and time string will be, but we still want it centered under our title. The easiest way to do this is to set the label’s Alignment attribute to Centered, and then stretch the label so that it fills the view from margin to margin (Figure 4.2).

Figure 4.2 Stretching a label to fill the view

image

Next, place a text field under the Current Date label. Stretch it so that it also fills the view from margin to margin. Set the attributes as shown in Table 4.1.

Table 4.1 Text Field Attributes

image

Whew, that’s a lot of settings. Let’s step through them one at a time. We want to restrict the input to valid decimal numbers. Also, since we’re only allowing numbers, it doesn’t make sense to enable autocorrection and capitalization. We may as well turn them off. The Numbers and Punctuation keyboard gives us all the keys we need (all the numbers, a Return key, and—for US English—a period). It actually allows too many characters, so we’ll need to filter our input. We’ll cover that in a bit.

The last two settings will help us create a more streamlined user interface. Essentially, we want to automatically create a new WeightEntry object and switch to the graph view as soon as the user presses the Return key. This reduces the total number of taps needed to enter a new weight. To help support this, we change the Return key’s label to Done to better communicate our intent. More importantly, auto-enabling the Return key means it will be disabled as long as the text field is empty. The system automatically enables the Return key once the user has entered some text. This—when paired with our input filtering—will guarantee that we have a valid weight entry whenever the Return key is pressed.

The interface should now match Figure 4.3. The basics are in place, but we’re going to spice it up a bit.

Figure 4.3 Completed enter weight view controller scene

image

As of iOS 4.1 there is a new keyboard type, the UIKeyboardTypeDecimalPad. This keyboard has only numbers, a decimal point, and a backspace. At first glance, this appears perfect for this project. However, it does not have a Return key—and we really want to allow the user to submit the weight value directly from the keyboard.

We could add a Return key (possibly in a custom input accessory view; see the Text, Web, and Editing Programming Guide for iOS), but that creates a more complex user interface.

iOS 5.0 adds another keyboard type, the UIKeyboardTypeTwitter. While not particularly useful for this project, the Twitter keyboard is optimized for writing tweets. It displays all the letters plus the @ and # keys.

Unfortunately, neither of these options appears in Interface Builder—at least, not in the current version of Xcode. Instead, you must set the text field’s keyboardType property in code. This isn’t hard. In the viewDidLoad method, just add self.weightTextField.keyboardType = UIKeyboardTypeDecimalPad; or self.weightTextField.keyboardType = UIKeyboardTypeTwitter;.


Set Autorotating and Autosizing

We will want our view to rotate into any orientation. There are two steps to this. First, we must modify the controller to allow autorotation.

Open EnterWeightViewController.m and navigate to the shouldAutorotateToInterfaceOrientation: method. This method should return YES if the controller’s view can rotate to the given interfaceOrientation. In the default implementation, it allows rotation only into the right-side-up portrait orientation.

To allow the app to rotate into any orientation, simply have the method return YES, as shown here:

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

However, if you run the app you may notice that autorotation still isn’t working. No matter how you twist the device, the interface remains locked in the portrait orientation.

That’s because there’s a trick. By default, tab views only allow autorotation if all of the contained view controllers support autorotation. Make the same change to the HistoryViewController and the GraphViewController, and then run the app again.

And...it’s still not working. OK, I lied. There were two tricks. When we created a custom tab view, it turns out that we inadvertently overrode the tab bar’s default shouldAutorotateToInterfaceOrientation: implementation. Open TabBarController.m, and delete the shouldAutorotateToInterfaceOrientation: method. This will restore the default behavior. Run it one last time. It should finally rotate as expected. Unfortunately, we have another problem (Figure 4.4). The UI elements are not properly resizing or repositioning themselves in the new, wider view.

Figure 4.4 Autorotation works, but autosizing does not.

image

To fix this, open MainStoryboard.storyboard again. Select the Enter Today’s Weight label, and open the Size inspector. The Autosizing control allows us to lock the object’s position relative to the top, bottom, left, or right sides. It also allows us to automatically stretch and shrink our control horizontally or vertically.

By default, the view is locked to the left and top. We want it locked to the top—but we want it to remain centered in the view. Click the red I-bar on the left side of the Autosizing control to turn it off. After you make changes in the Autosizing control, check the Example preview to make sure the control will behave as you expect. In this case, it should remain centered at the top of the view as the size changes (Figure 4.5).

Figure 4.5 Locking the control to the top of the view

image

We want the Current Date and Time label to stretch so it fills the view from margin to margin. Select the label and then lock it to the top, left, and right sides. Also, turn on the horizontal resizing as shown in Figure 4.6. Use the same settings for the text field as well.

Figure 4.6 Stretching the control to fill the view horizontally

image

Run the app again. Now when it rotates into landscape mode, the app should position and resize the controls appropriately (Figure 4.7).

Figure 4.7 Correct autorotation and autosizing

image

Tapping the text field will automatically display the specified keyboard. The system slides the keyboard up from the bottom, covering the lower portion of your content. This will not trigger any autosizing behaviors (the view’s size remains unchanged, the bottom portion is simply covered up). You need to make sure your users can see the selected text field and reach your controls. This can create problems, especially in landscape orientation, since the keyboard will cover most of the screen.

In our EnterWeightView, we deliberately positioned all the controls so that they are still visible even in landscape orientation. However, this has two limitations: It makes the view appear somewhat top-heavy, and it’s only possible for the simplest views.

Most of the time, you will need to manually reposition or resize the controls (typically using a scroll view) so that the selected control is visible. The keyboard sends out a number of notifications that you can use to make these modifications: UIKeyboardWillShowNotification, UIKeyboardDidShowNotification, UIKeyboardWillHideNotification, and UIKeyboarddidHideNotification.


Adding Outlets and Actions

Let’s start by opening EnterWeightViewController.h. We want our controller to conform to the UITextFieldDelegate protocol. This will let us respond to changes in our text field. Modify the header as shown here:

@interface EnterWeightViewController : UIViewController
    <UITextFieldDelegate> {
}

Switch back to MainStoryboard.storyboard and open the Assistant editor. Make sure EnterWeightViewController.h is shown in the second panel. Then right-click the text field, and drag a connection from the delegate to the view controller icon in the scene’s dock (Figure 4.8).

Figure 4.8 Connecting the text field’s delegate

image

Next, drag the Did End On Exit event to the header file and create a new action (Figure 4.9). Name the action saveWeight.

Figure 4.9 Creating a new action

image

Dismiss the connections pop-up window, and Control-drag the text field to the header file (Figure 4.10). Create a new strong outlet named weightTextField.

Figure 4.10 Creating a new outlet

image

Finally, Control-drag from the Current Date and Time label and create a strong outlet named dateLabel.


The format that Xcode 4.0 uses when naming auto-generated outlets may vary. In particular, it adds the underscore before the instance variable’s name if you have already synthesized another property with an underscore. In our EnterWeightViewController class, we previously added the weightHistory property, which uses the implicit _weightHistory instance variable. Therefore, our auto-generated outlets also use underscores. Unfortunately, if you don’t have any existing properties with leading underscores, it simply uses the default naming: the instance variable and property names are the same.

Therefore, the properties generated automatically by Xcode may not match the advice I gave in Chapter 2. For the purposes of this book, I won’t go through the extra steps required to rename the instance variables. However, my own code is somewhat different; I generally prefer to keep things tidy. As always, your mileage may vary.


The header file should now appear as shown here:

#import <UIKit/UIKit.h>
@class WeightHistory;
@interface EnterWeightViewController : UIViewController
<UITextFieldDelegate>
@property (nonatomic, strong) WeightHistory* weightHistory;
@property (strong, nonatomic) IBOutlet UITextField *weightTextField;
@property (strong, nonatomic) IBOutlet UILabel *dateLabel;
- (IBAction)saveWeight:(id)sender;
@end

Creating the Unit Button

Now we want to add a unit button inside the text field. This will allow us to both display the current unit type and change units.

Our unit button is somewhat odd. We won’t be adding it directly to our view hierarchy. Instead, we will programmatically assign it to our text field’s rightView property. This is not something we can do in Interface Builder.

Of course, we could still create the button in Interface Builder and assign it as a new top-level object. We could then use Interface Builder to configure its settings. Unfortunately, this doesn’t really help us, since we won’t be able to visually inspect it as we edit it. All things considered, it’s probably easiest to just create the button in code.

Open EnterWeightViewController.h and add the following property and action:

@property (nonatomic, strong) WeightHistory* weightHistory;
@property (strong, nonatomic) IBOutlet UITextField *weightTextField;
@property (strong, nonatomic) IBOutlet UILabel *dateLabel;
@property (strong, nonatomic) UIButton* unitsButton;
- (IBAction)saveWeight:(id)sender;
- (IBAction)changeUnits:(id)sender;

Next, switch to the implementation file and synthesize the property.

@synthesize unitsButton=_unitsButton;

Now, scroll down to the viewDidLoad method. Modify it as shown:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.unitsButton = [UIButton buttonWithType:UIButtonTypeCustom];
    self.unitsButton.frame = CGRectMake(0.0f, 0.0f, 25.0f, 17.0f);
    self.unitsButton.backgroundColor = [UIColor lightGrayColor];
    self.unitsButton.titleLabel.font =
    [UIFont boldSystemFontOfSize:12.0f];
    self.unitsButton.titleLabel.textAlignment =
    UITextAlignmentCenter;
    [self.unitsButton setTitle:@"lbs"
                      forState:UIControlStateNormal];
    [self.unitsButton setTitleColor:[UIColor darkGrayColor]
                           forState:UIControlStateNormal];
    [self.unitsButton setTitleColor:[UIColor blueColor]
                           forState:UIControlStateHighlighted];
    [self.unitsButton addTarget:self
                         action:@selector(changeUnits:)
               forControlEvents:UIControlEventTouchUpInside];
    self.weightTextField.rightView = self.unitsButton;
    self.weightTextField.rightViewMode = UITextFieldViewModeAlways;}

We start by creating a custom button. Then we configure it. It’s 17 points tall and 25 points wide, with a light gray background. We also configure its title. It uses a 12-point bold system font and is center aligned. It’s important to center the title; as the user changes units, we will switch the button’s label between “lbs” and “kg.” Centering the label gives the button a nice, consistent appearance, even when the label’s size changes.

Next, we set the default title to “lbs” and then assign text colors for the different control states. Normally the text is dark gray, but when the button is highlighted, the text turns blue.

We also assign the changeUnits: action to our button’s UIControlEventTouchUpInside event. This is identical to drawing a connection between a button’s TouchUpInside event and the desired action in Interface Builder. When the unitsButton is touched, the system will call changeUnits:.

Finally, we assign the unit button to the text field’s rightView property. This will cause it to appear inside the text field along the right side. We then set the view mode so that our button is always visible.

As always, since we assigned the button in viewDidLoad, we should clear it in viewDidUnload. Navigate down to viewDidUnload and add the following line:

self.unitsButton = nil;

Finally, add a method stub for our changeUnits: action. We will flesh out this method after adding the change units view to our storyboard.

- (IBAction)changeUnits:(id)sender {
    // method stub
}

Implementing Actions and Callback Methods

Now switch to EnterWeightViewController.m. Before we start tackling the action and delegate methods, let’s add an extension with a couple of private properties.

@interface EnterWeightViewController()
@property (nonatomic, strong) NSDate* currentDate;
@property (nonatomic, strong) NSNumberFormatter* numberFormatter;
@end

We’re adding two properties: currentDate will hold the current date (based on the date and time when the view appeared), and we will use the numberFormatter to process our user’s input.

Next, synthesize these properties.

@synthesize currentDate = _currentDate;
@synthesize numberFormatter = _numberFormatter;

Then, navigate to the viewDidLoad method. Add the following code to instantiate and configure our number formatter:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.numberFormatter = [[NSNumberFormatter alloc] init];
    [self.numberFormatter
     setNumberStyle:NSNumberFormatterDecimalStyle];
    [self.numberFormatter
     setMinimum:[NSNumber numberWithFloat:0.0f]];
    self.unitsButton = [UIButton buttonWithType:UIButtonTypeCustom];
    self.unitsButton.frame = CGRectMake(0.0f, 0.0f, 25.0f, 17.0f);
    self.unitsButton.backgroundColor = [UIColor lightGrayColor];
    ...
}


Both setTitle:forState: and setTitleColor:forState: refer to the button’s state. Every control has a state: normal, highlighted, selected, or disabled. Different actions can change the control’s state (see Table 4.2). Our button uses only two. It is highlighted when touched; otherwise, it is normal.

Table 4.2 Control States

image

When setting per-state attributes, if you don’t explicitly assign a value to a given state, that state will default back to the UIControlStateNormal setting. For example, in our code we only had to set the title once. Unless we say otherwise, every other state will use this value as well.

This brings up a point that is often confused. If you look up UIControlState in the developer documentation, the enum is designed as a bitmask. This means we should be able to combine multiple states using the bitwise OR operator (|). As a result, you might be tempted to explicitly set the title for all the states using code like this:

UIControlState all =  UIControlStateNormal |
                      UIControlStateSelected |
                      UIControlStateHighlighted |
                      UIControlStateDisabled;
[self.button setText:@"default text" forState:all]

Unfortunately, this does not work. Instead of setting the title for all states, you are actually setting the title for a single state: specifically, the state in which the control is selected, highlighted, and disabled—all at the same time (in this case, UIControlStateNormal is ignored).

This can be handy when creating things like custom two-state buttons. These buttons have an ON and an OFF state, and each of these states can be displayed either normally or highlighted. This means we need to define four different appearances for these four different states.

We can use the button’s selected property to indicate its ON/OFF state. Our appearances are then defined as follows: UIControlStateNormal means the button is OFF; UIControlStateHighlighted means the button is OFF and touched; UIControlStateSelected means the button is ON; and UIControlStateHighlighted | UIControlStateSelected means the button is both ON and touched (Figure 4.11). Note that UIControlStateDisabled is not used here.

Figure 4.11 UIControlState changes for a two-state button

image

However, if you’re simply trying to set a default appearance across all the states, just use UIControlStateNormal and let the others default back to it.


In the last chapter, we used number formatters to create number strings that would format properly regardless of the device’s language and country settings. In this chapter, we will see the other side. We will use the NSNumberFormatter to verify and filter the user’s input. Here, we set it to accept only positive decimal numbers. We will also use the formatter to parse the user input, converting it from a string into a float value.

Again, anything we set up in viewDidLoad needs to be torn down in viewDidUnload. Add the following line:

- (void)viewDidUnload
{
    [self setWeightTextField:nil];
    [self setDateLabel:nil];
    self.unitsButton = nil;
    self.numberFormatter = nil;
    [super viewDidUnload];
}

We still need to reset the screen each time it appears. Remember, a view might be created only once but appear many times. Actually, it’s even more complicated than this. A view may be loaded and unloaded multiple times (usually due to memory shortages). Each time it is loaded, it may appear onscreen more than once. Therefore, it’s important to think things through. Which configuration items need to be performed once and only once? These are typically performed in the application delegate’s application:didFinishLaunchingWithOptions: method. Which configuration items should be performed each time a view loads? These should be performed in the view controller’s viewDidLoad method. Finally, which ones should be performed every time the view appears onscreen? These are done in the viewWillAppear: or viewDidAppear: method.


The viewWillAppear: and viewDidAppear: methods can be somewhat confusing. For the most part, the system calls these methods whenever a view controller is used to display another view controller. This includes displaying modal views, adding new views to a navigation controller, or switching between views on a tab bar. The system will even call these notification methods when a view reappears. For example, when returning from a modal view or when a covering view is popped off the navigation stack.

However, manually adding a view using the addSubview: method does not trigger these notifications. Nor does changing a view’s hidden or alpha properties to hide or show a view. And, just to make things even more confusing, adding a view to the application’s window using addSubview: will trigger the notifications (though in iOS 4.0 and later, you should assign the view controller to the window’s rootViewController property instead).

While these rules and exceptions may seem confusing, they do point to a common design problem. If your view appears onscreen but your view controller’s viewWillAppear: and viewDidAppear: methods are not called, you may have an inconsistent view hierarchy. This occurs when you add a view to the view hierarchy without properly adding its view controller to the view controller hierarchy.

There are a number of ways to fix this. Most simply, you could get rid of the view controller and add the view directly to its superview’s controller. After all, view controllers often manage more than one view at a time. We’re doing this anytime we create an outlet for one of our controls.

Alternatively, in iOS 5.0 we could create a custom container view controller and use it to present our subview’s controller. As the name suggests, a container view controller manages one or more other view controllers. Examples in UIKit include UINavigationController, UITabViewController, and UIPageViewController. iOS 5 has extended the UIViewController class, giving us methods to add (addChildViewController:) or remove (removeFromParentViewController) controllers from the controller hierarchy. This allows us to create a controller hierarchy that mirrors our views. As a result, the system can correctly forward important messages along both hierarchies.

For more information, check out the UIViewController documentation. In particular, look at the automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers property. This contains a list of all the messages that need to be forwarded along the view controller hierarchy. We will also create our own custom container view controller in the “Creating a UIViewController Container” section of Chapter 8.


In our case, we want to update the current date and make sure our text field is ready to receive new information. Implement the viewWillAppear: method as shown:

- (void)viewWillAppear:(BOOL)animated {
    // Sets the current time and date.
    self.currentDate = [NSDate date];
    self.dateLabel.text =
        [NSDateFormatter localizedStringFromDate:self.currentDate
            dateStyle:NSDateFormatterLongStyle
            timeStyle:NSDateFormatterShortStyle];
    // Clear the text field.
    self.weightTextField.text = @"";
    [self.weightTextField becomeFirstResponder];
    [super viewWillAppear:animated];
}

Here we create a new NSDate object set to the current date and time. We then use the NSDateFormatter class method localizedStringFromDate:dateStyle:timeStyle: to produce a properly localized string representation. As you might expect, the formatting of dates and times also varies greatly from country to country and language to language. The NSDateFormatter lets us easily create date strings based on the device’s language and region settings.


Note

image

Both NSNumberFormatter and NSDateFormatter are subclasses of the NSFormatter abstract class. NSFormatter is designed to act as a base class for objects that format, validate, or parse strings. For more information on using formatters or even creating your own, see the Data Formatting Guide in Apple’s documentation.


Next, we clear the text field and make it the first responder. Making a text field the first responder will automatically display the keyboard. Now, we’ve already linked the text field’s Did End On Exit event to the saveWeight: method. This method will be called whenever the keyboard’s Done button is pressed.

As we described earlier, this provides a very streamlined system for entering the weights. When the user opens this view, the text field is automatically selected and the keyboard is ready and waiting. The user just types in the weight value and presses Done. They don’t need to select the text box or press a Save button. Everything is simple, automatic, and clean.

However, it does create one small problem. The keyboard covers our tab bar. This prevents our users from navigating away from this screen without entering a new weight.

Obviously, this is not ideal. We need to provide a way (preferably something intuitive and non-intrusive) to dismiss the keyboard, giving us access to the tab bar again. Let’s add a gesture recognizer that responds to a simple down swipe.

Open MainStoryboard.storyboard again. Drag a swipe gesture recognizer from the library and drop it onto the enter weight view controller scene’s main view (Figure 4.12).

Figure 4.12 Adding a swipe gesture recognizer

image

The gesture recognizer will appear in the scene’s dock. Select it and open the Attributes inspector. Set the Swipe attribute to Down. Leave the Touches attribute at 1. This will now trigger on a single-finger, downward swipe.

If we’re going to recognize downward swipes, we should recognize upward swipes as well. So, let’s add a second recognizer. Drag out another swipe gesture recognizer and add it to the view. Set its Swipe attribute to Up. Leave the Touches attribute at 1.

Now open the Assistant editor, and make sure the EnterWeightViewController.h file is showing. Right-click and drag from the down swipe gesture recognizer to just below the declaration of our changeUnits: method (Figure 4.13). Change the Connection to Action, and name it handleDownwardSwipe. Then do the same for the up gesture recognizer. Name its action handleUpwardSwipe.

Figure 4.13 Connecting a gesture recognizer

image

Then create an outlet for each gesture recognizer. Control-drag from the recognizer to the header file. Name the first outlet downSwipeRecognizer. Name the second upSwipeRecognizer.

Now switch to EnterWeightViewController.m and implement the actions:

- (IBAction)handleDownwardSwipe:(id)sender {
    // Get rid of the keyboard.
    [self.weightTextField resignFirstResponder];
}
- (IBAction)handleUpwardSwipe:(id)sender {
    // display keyboard
    [self.weightTextField becomeFirstResponder];
}

The handleDownwardSwipe method simply has the text field resign as first responder. Just as before, the keyboard is automatically linked to the first responder. When the text field resigns, the keyboard disappears. The handleUpwardSwipe method is just the inverse of that. It assigns the text field as the first responder, causing the keyboard to appear again. Of course, the user could do the same thing by simply tapping the text field, but many users will automatically try to undo a downward swipe with an upward swipe. Adding the inverse operation makes the interface feel more complete.

While this is an elegant solution, it brings up a common problem with iOS development. We can easily build complicated touch-, gesture-, and motion-based controls (see Chapter 8 for more examples), but how do we make sure the user knows they exist? iOS applications usually don’t have help screens, and—in my experience—few users actually read the help information that does exist.

For example, you might create a great three-finger swipe that radically simplifies your application’s workflow. However, unless your users stumble upon it by accident, most will never know it exists. That’s not to say that you should avoid using unusual gestures. On the contrary, many applications use novel gestures to great effect. The Twitter app is an excellent example: You scroll through the table view of incoming tweets. When you get to the top, you just pull down to check for new messages.

This is a brilliant gesture. Users will almost certainly stumble upon it as they accidentally try to scroll past the end of their tweets. More importantly, once you find it, the gesture is so natural that it quickly becomes part of your regular workflow.

The bottom line is that successfully communicating how your app operates can be one of the biggest challenges in iOS development. Typically, this involves extensive usability testing to make sure your interface is as intuitive and natural as possible.

OK, let’s switch gears and tackle the saveWeight: action.

#pragma mark – Action Methods
- (IBAction)saveWeight:(id)sender {
    // Save the weight to the model.
    NSNumber* weight = [self.numberFormatter
                        numberFromString:self.weightTextField.text];
    WeightEntry* entry = [[WeightEntry alloc]
                          initWithWeight:[weight floatValue]
                          usingUnits:self.weightHistory.defaultUnits
                          forDate:self.currentDate];
    [self.weightHistory addWeight:entry];
    // Automatically move to the second tab.
    // Should be the graph view.
    self.tabBarController.selectedIndex = 1;
}

First, we parse the text field to extract the weight’s floating point value. Normally you want to check numberFromString:’s return value. If the number does not match the specified format, this method will return nil. However, in this case we know that the text field can only have valid values. The saveWeight: action is only triggered when the keyboard’s Done button is pressed, and the Done button only becomes active when our text field contains text. Since we will be filtering the user input, this text can only contain a valid decimal number.

Next, we instantiate a new WeightEntry object using this weight value, our defaultUnits, and the currentDate property (if you remember, currentDate was set when the enter weight view appeared onscreen). We add this entry to our model.

Finally, we change the tab bar’s selected controller. This will automatically move us to the second tab—currently set to the graph view. Again, we are trying to make entering new weights as streamlined as possible. For the most part, this means removing unnecessary touches. Users will typically enter only one weight at a time. Therefore, we should streamline their interaction and automatically bring up the weight trends graph after each new value.

We’re going to skip the changeUnits: method for now. We’ll get back to it in the “Changing Weight Units” section. Instead, let’s begin filtering the user’s input.

Filtering Keyboard Input

The UITextFieldDelegate protocol has a number of optional methods that we can use to monitor and control our text field. In particular, we will implement textField:shouldChangeCharactersInRange:replacementString: to filter the user input. Implement the method as shown:

#pragma mark - Delegate Methods
- (BOOL)textField:(UITextField *)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string {
    // It's OK to hit return.
    if ([string isEqualToString:@" "]) return YES;
    NSString* changedString =
    [textField.text stringByReplacingCharactersInRange:range
                                            withString:string];
    // It's OK to delete everything.
    if ([changedString isEqualToString:@""]) return YES;
    NSNumber* number =
    [self.numberFormatter numberFromString:changedString];
    // Filter out invalid number formats.
    if (number == nil) {
        // We might want to add an alert sound here.
        return NO;
    }
    return YES;
}

This method is called whenever the user presses a button on the keyboard (including the backspace button). If we return YES, the change is made. If we return NO, the change is canceled.

We start by checking to see if the user hit the Return key. Since this is the trigger for our saveWeight: action, we need to accept it.

Next, we create a new string by applying the proposed change to textField’s current contents. If the resulting string represents a valid decimal number, we accept the change. Otherwise, we reject it.

Of course, it’s not quite that simple. First, we have to deal with another corner case. If the resulting string is empty, we allow the change. Technically, an empty string is not a valid decimal number; however, we really want to let the users delete all the characters, just in case they made a typing mistake and want to start over.

If the string is not empty, we use our numberFormatter to parse our string. Again, we use the numberFromString: method. If the string does not match the expected format, this method returns nil. We simply check the return value and return YES or NO as appropriate.

Technically, we could simplify the code and just return the result from parsing the string as shown here:

return [self.numberFormatter numberFromString:changedString];

However, we may want to add an alert sound or other feedback to the user. Using the more verbose version of the code will make those additions easier.

Run the application. The text field should appear with the embedded unit button. Check to make sure the input filtering works correctly. When you press the Done button, the view should switch to the graph view. Nothing shows up yet (of course), but the transition should work. Go back to the enter weight view. The system should automatically clear and select the text field. The keyboard should be visible. Swipe down to dismiss the keyboard. Swipe up to re-enable it. You can even tap the units button, but it won’t do anything yet. We’ll fix that next (Figure 4.14).

Figure 4.14 The completed enter weight view

image

Changing Weight Units

When the user presses the units button, we need to open a new view and let them change the default units. To do this, we will use a modal view. Modal views are perfect for presenting focused, short-term tasks that the user must either complete or explicitly cancel.

Let’s start by adding another UIViewController subclass. Name it UnitSelectorViewController and place it in the Controllers group (if you need help, follow the step-by-step instructions in “Configuring the Tab Bar” in Chapter 3).

Now open MainStoryboard.storyboard. Drag out a new UIViewController object and place it next to our enter weight view controller scene. Switch to the Identity inspector, and change its Class setting so that it uses our new UnitSelectorViewController class.

Ideally, we want a segue from our changeUnits: action to the unit selector view. Unfortunately, we cannot draw segues from actions directly. Instead, let’s create a segue that we can manually call from our code.

Control-drag from the enter weight view controller icon to our new scene. In the pop-up window, select modal. This creates a generic modal storyboard segue. This sets the segue between the two scenes. Select the segue and switch to the Attributes inspector. Set the Identifier attribute to Unit Selector Segue, and the Transition attribute to Flip Horizontal. Now we just need to trigger this segue from our changeUnits: action.

Switch back to EnterWeightViewController.m. Let’s start by defining a string constant for our segue’s identifier. Add the following line before the @implementation block:

static NSString* const UNIT_SELECTOR_SEGUE = @"Unit Selector Segue";

Now navigate down to the changeUnits: action. We just need to call our controller’s performSegueWithIdentifier:sender: method.

- (IBAction)changeUnits:(id)sender {
    [self performSegueWithIdentifier:
     UNIT_SELECTOR_SEGUE sender:self];
}

This will trigger our segue. Our enter weight view will flip over and reveal the new unit selector view.

So, let’s design that view. Open the storyboard again, and zoom in on our unit selector scene. Select the view and change its Background attribute to View Flipside Background Color. Next, drag a picker view from the Object library and place it at the top of the view. In the Size inspector, make sure it is locked to the left, right, and top and that it scales horizontally.

Next, drag out a button and place it at the bottom of the view. Stretch it so that it fills the view from margin to margin. Its autosizing settings should lock it to the left, bottom, and right, with horizontal scaling enabled. Finally, set its Title attribute to Done.

We ideally want a colored button. The iOS 5 SDK gives us a number of functions for customizing the appearance of our controls. Unfortunately, this doesn’t include setting a button’s background color. There are a number of ways to work around this. For example, many developers create stretchable background images for their buttons. However, this does not give us very much control at runtime. We could create a UIButton subclass and provide custom drawing code—but that’s a lot of work. Instead, we’ll modify the button’s appearance using Core Animation (while also looking at some of the problems with this approach).

Change the button’s Type attribute to Custom, and set the Background attribute to a dark green. I selected Clover from the crayon box color selector. Click the Background attribute. When the pop-up menu appears, select Other. Make sure the crayon box tab is selected, and then choose the Clover crayon (third from the left on the top row).

Finally, set the Text Color attribute to Light Text Color.

The interface should now match Figure 4.15. Everything looks OK—except for the square corners on our Done button. We’ll fix that shortly. In the meantime, let’s set up our outlets and actions.

Figure 4.15 The unit selector view

image

First, open UnitSelectorViewController.h. This class needs to adopt both the UIPickerViewDelegate and the UIPickerViewDataSource protocols.

@interface UnitSelectorViewController : UIViewController
    <UIPickerViewDelegate, UIPickerViewDataSource> {
}
@end

Then switch back to the storyboard file, and open the Assistant editor. Make sure UnitSelectorViewController.h is showing. Control-drag the picker view to the header file, and create a strong outlet named unitPickerView. Next, Control-drag the button twice. First, create a strong outlet named doneButton. Then create an action named done. Make sure its Event is set to Touch Up Inside.

Now we need to link the picker view to its delegate and data source. Right-click the picker view, and then drag from the pop-up window’s delegate outlet to the view controller icon in the scene’s dock (Figure 4.16). Next, drag the pop-up’s dataSource outlet to the view controller as well.

Figure 4.16 Connecting the delegate

image

Defining the View Delegate

Switch back to the Standard editor and the UnitSelectorViewController.h file. It should now appear as shown here:

#import <UIKit/UIKit.h>
@interface UnitSelectorViewController : UIViewController
<UIPickerViewDelegate, UIPickerViewDataSource>
@property (strong, nonatomic) IBOutlet UIPickerView *unitPickerView;
@property (strong, nonatomic) IBOutlet UIButton *doneButton;
- (IBAction)done:(id)sender;
@end

We still need to make a few additional changes. Start by importing our WeightEntry class, and add a forward declaration for the UnitSelectorViewControllerDelegate protocol before its @interface block.

#import <UIKit/UIKit.h>
#import "WeightEntry.h"
@protocol UnitSelectorViewControllerDelegate;
@interface UnitSelectorViewController :
UIViewController <UIPickerViewDelegate, UIPickerViewDataSource> {
...

Now let’s declare two additional properties: one for our delegate, the other for our default unit.

@property (strong, nonatomic) IBOutlet UIButton *doneButton;
@property (weak, nonatomic) id<UnitSelectorViewControllerDelegate>
    delegate;
@property (assign, nonatomic) WeightUnit defaultUnit;
- (IBAction)done:(id)sender;

Finally, we need to define our protocol. Add the following, after the @interface block:

@protocol UnitSelectorViewControllerDelegate <NSObject>
- (void)unitSelectorDone:(UnitSelectorViewController*)controller;
- (void)unitSelector:(UnitSelectorViewController*)controller
        changedUnits:(WeightUnit)unit;
@end

That’s it. We’re done with the interface. Now we need to implement these methods.

Implementing the Controller

Open UnitSelectorViewController.m and synthesize the delegate and the defaultUnit.

@synthesize delegate = _delegate;
@synthesize defaultUnit = _defaultUnit;

Now we want to automatically select the current default unit when our view loads. To do this, uncomment viewDidLoad and make the following changes:

#pragma mark - View lifecycle
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Set the default units.
    [self.unitPickerView selectRow:self.defaultUnit
                       inComponent:0
                          animated:NO];}

Remember, our WeightUnit enum values are assigned sequentially starting with 0. Our picker view is also zero-indexed, with exactly one row for each WeightUnit value. This means we can use WeightUnits and row indexes interchangeably. Each row index maps to a corresponding WeightUnit. Here, we simply select the row that corresponds with the default unit value.

Next, we need to enable autorotation to any orientation. As before, simply have the shouldAutorotateToInterfaceOrientation: method return YES.

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

Now let’s implement the done: action. We just call the delegate’s unitSelectorDone: method, as shown here:

- (IBAction)done:(id)sender {
    [self.delegate unitSelectorDone:self];
}

OK, we’re almost done with this controller. We still need to implement the UIPickerViewDataSource methods:

#pragma mark - UIPickerViewDataSource Methods
- (NSInteger)numberOfComponentsInPickerView:
(UIPickerView *)pickerView {
    return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView
    numberOfRowsInComponent:(NSInteger)component {
    return 2;
}

The numberOfComponentsInPickerView: method simply returns the number of components that our picker view will use. Each component represents a separate settable field. For example, a date picker has three components: month, day, and year. In our case, we only need a single component.

The pickerView:numberOfRowsInComponent: method returns the number of rows (or possible values) for the given component. We know that our picker view only has a single component, so we don’t need to check the component argument. Since we only have two possible values, pounds and kilograms, we can just return 2.

Now let’s look at the delegate methods:

#pragma mark - UIPickerViewDelegate Methods
- (NSString *)pickerView:(UIPickerView *)pickerView
             titleForRow:(NSInteger)row
            forComponent:(NSInteger)component {
    return [WeightEntry stringForUnit:row];
}
- (void)pickerView:(UIPickerView *)pickerView
      didSelectRow:(NSInteger)row
       inComponent:(NSInteger)component {
    [self.delegate unitSelector:self changedUnits:row];
}

The pickerView:titleForRow:forComponent: method should return the title that will be displayed for the given row and component. In our case, we can map the rows directly to the WeightUnit enum values and simply call stringForUnit: to generate the correct string (@"lbs" or @"kg").

Meanwhile, the pickerView:didSelectRow:inComponent: method is called whenever the user changes the current selection. Here, we simply call the delegate’s unitSelector:changedUnits: method, passing in the row value. Again, our row values correspond directly to the appropriate WeightUnit values.

Passing Data Back and Forth

We still need to pass data into and out of our modal view. Start by opening EntryWeightViewController.h. We need to import UnitSelectorViewController.h and declare that EnterWeightViewController will adopt the UnitSelectorViewControllerDelegate protocol.

#import <UIKit/UIKit.h>
#import "UnitSelectorViewController.h"
@class WeightHistory;
@interface EnterWeightViewController : UIViewController
<UITextFieldDelegate, UnitSelectorViewControllerDelegate> {

Now switch to the implementation file. We trigger the modal segue in our changeUnits: method—but we cannot set the default unit value there. Our destination view controller may not exist yet. Instead, we wait for the prepareForSegue:sender: method—just as we did in Chapter 3.

- (void)prepareForSegue:(UIStoryboardSegue *)segue
                 sender:(id)sender {
    if ([segue.identifier isEqualToString:UNIT_SELECTOR_SEGUE]) {
        UnitSelectorViewController* unitSelectorController =
            segue.destinationViewController;
        unitSelectorController.delegate = self;
        unitSelectorController.defaultUnit =
           self.weightHistory.defaultUnits;
    }
}

Here, we check the segue’s identifier, just to make sure we have the correct segue. Then we grab a reference to our UnitSelectorViewController, and we set both the delegate and the default unit value.

To get data from our modal view, we simply implement the UnitSeletorViewControllerDelegate methods. Let’s start with unitSelector:changedUnits:.

-(void)unitSelector:(UnitSelectorViewController*) sender
       changedUnits:(WeightUnit)unit {
    self.weightHistory.defaultUnits = unit;
    [self.unitsButton setTitle: [WeightEntry stringForUnit:unit]
                      forState:UIControlStateNormal];}

This method is called whenever the user changes the units in the UnitSelectorViewController. Here, we tell the model to change its default units and then update the title in our unit button. Again, we only update the title for the UIControlStateNormal. All other control states will default back to this setting.

Now let’s look at unitSelectorDone:.

-(void)unitSelectorDone:(UnitSelectorViewController*) sender {
    [self dismissModalViewControllerAnimated:YES];
}

This method is called when the user presses the UnitSelectorViewController’s Done button. Note that we could have dismissed the modal view within UnitSelectorViewController’s done: method by calling [self.parentViewController dismissModalViewControllerAnimated:YES]. However, the pattern we’re using here is generally best.

Passing control back to the parent view through a delegate method and then letting the parent view dismiss the modal view may take a bit more code, but it also gives us additional flexibility. For example, our parent controller might want to access the delegate view’s properties before dismissing it. Or we may want to perform some postprocessing after dismissing the modal view. We can easily add these features in our delegate method. Additionally, it just feels cleaner. If a class presents a modal view, then it should also dismiss that view. Splitting the presentation and dismissal code into different classes makes everything just a little harder to follow.

And this isn’t an entirely academic argument. As our code is currently written, we will change our default unit value whenever the user changes the value in the picker view. However, this is not necessarily the best approach. We may want to wait until the user presses the Done button, and then set the default unit value based on their final selection. With the delegate methods in place, we can easily change our implementation based on actual performance testing. More importantly, we can change this behavior in our EnterWeightViewController class; we don’t need to touch our modal view at all.


Note

image

Most delegate methods will follow the format shown for the UnitSelectorViewControllerDelegate. The method name begins with a description of the delegating class (e.g., tableView, pickerView, unitSelector), and the first argument is a reference back to the delegating class. While you may not use the reference, having it can greatly simplify your delegate methods. For example, for modal view delegates you don’t need to assign your modal view to an instance method—you can access it through the delegating reference instead.


Run the application. You should be able to press the unit button and bring up the unit selector view. Change the units to kilograms and press Done. The button’s title should change from “lbs” to “kg.” There’s only one problem remaining: Our Done button still looks chunky. Let’s fix that.

Rounding Corners with Core Animation


Note

image

Manipulating views with Core Animation is an advanced topic. While this represents a useful technique for modifying the appearance of controls without having to subclass them, it can get a bit complicated. However, since it doesn’t add any critical features to the application, you can safely skip this section. Your application will still function normally.


Most of the time when you talk about Core Animation, you’re talking about smoothly moving user interface elements around the screen, having them fade in and out, or flipping them over. Here, however, we will hijack some of the more obscure features of Core Animation to round off our button’s corners, add a border, and layer over a glossy sheen.

As you might guess, Core Animation is a deep and complex subject. We will look at techniques for animating view properties in “Managing Pop-Up Views” in Chapter 8. However, even that only scratches the surface. If you want to get all the gory details, I recommend reading the Core Animation Programming Guide in Apple’s documentation.

First things first, we need to add the QuartzCore framework to our project. Click the blue Health Beat icon to bring up the project settings. Make sure the Health Beat target is selected, and click the Build Phases tab. Next, expand the Link Binary With Libraries build phase, and click the plus button to add another framework. Scroll through the list and add QuartzCore.framework (Figure 4.17).

Figure 4.17 Adding a new framework

image

Next, open UnitSelectorViewController.m. We need to import the QuartzCore header.

#import <QuartzCore/QuartzCore.h>

Then navigate to the viewDidLoad method. Modify it as shown:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Set the default units.
    [self.unitPickerView selectRow:self.defaultUnit
                       inComponent:0
                          animated:NO];
    //Build our gradient overlays.
    CAGradientLayer* topGradient = [[CAGradientLayer alloc] init];
    topGradient.name = @"Top Gradient";
    // Make it half the height.
    CGRect frame = self.doneButton.layer.bounds;
    frame.size.height /= 2.0f;
    topGradient.frame = frame;
    UIColor* topColor = [UIColor colorWithWhite:1.0f alpha:0.75f];
    UIColor* bottomColor = [UIColor colorWithWhite:1.0f alpha:0.0f];
    topGradient.colors = [NSArray arrayWithObjects:
                          (__bridge id)topColor.CGColor,
                          (__bridge id)bottomColor.CGColor, nil];
    CAGradientLayer* bottomGradient =
    [[CAGradientLayer alloc] init];
    bottomGradient.name = @"Bottom Gradient";
    // Make it half the size.
    frame = self.doneButton.layer.bounds;
    frame.size.height /= 2.0f;
    // And move it to the bottom.
    frame.origin.y = frame.size.height;
    bottomGradient.frame = frame;
    topColor = [UIColor colorWithWhite:0.0f alpha:0.20f];
    bottomColor = [UIColor colorWithWhite:0.0f alpha:0.0f];
    bottomGradient.colors = [NSArray arrayWithObjects:
                             (__bridge id)topColor.CGColor,
                             (__bridge id)bottomColor.CGColor, nil];
    // Round the corners.
    [self.doneButton.layer setCornerRadius:8.0f];
    // Clip sublayers.
    [self.doneButton.layer setMasksToBounds:YES];
    // Add a border.
    [self.doneButton.layer setBorderWidth:2.0f];
    [self.doneButton.layer
     setBorderColor:[[UIColor lightTextColor] CGColor]];
    // Add the gradient layers.
    [self.doneButton.layer addSublayer:topGradient];
    [self.doneButton.layer addSublayer:bottomGradient];
}

There’s a lot going on here, so let’s step through it slowly. This code modifies our button’s Core Animation layer. The CALayer is a lightweight object that encapsulates the timing, geometry, and visual properties of a view. In UIKit, a CALayer backs each UIView (and therefore, anything that inherits from UIView). Because of the tight coupling between layers and views, we can easily access and change the visual properties contained in our button’s layer.

We start by creating a CAGradientLayer and giving the layer a name. We will use this name to identify our gradient layer in later methods. Next, we calculate the frame for this layer. We start with the button layer’s bounds, but we divide the height in half. Remember, the frame is the object’s coordinates and size in the containing view or layer’s coordinate system. The bounds represent the object’s coordinates and size in its own coordinate system. In other words, the origin is almost always set to {0.0f, 0.0f} (there are situations where you might use a non-zero origin as an offset, for example when clipping part of an image, but these cases are rare). Using the superlayer’s bounds for the sublayer’s frame means the sublayer will fill the superlayer completely. By dividing the height in half, we end up with a sublayer that will cover just the top half of the main layer.

CAGradientLayers accept an NSArray filled with CGColorRefs. By default, it creates a linear, evenly spaced gradient that transitions from one color to the next. We will just pass in two colors, which will represent the two end points.

Note that an NSArray technically only accepts pointers to Objective-C objects. A CGColorRef is simply a pointer to a CGColor structure—definitely not an Objective-C object. However, we can cast them into id objects to get around the compiler warnings. It’s a bit wacky, but we do what we have to do.

For the curious, this works because NSArray is toll-free bridged with the Foundation CFArray class (under the surface, they are the same objects). While NSArrays are only used to store Objective-C objects, CFArrays can be used to store any arbitrary pointer-sized data. In fact, the CFArrayCreate() method includes parameters that define how the objects are (or are not) retained when placed in the array. When we create an NSArray, we are really creating a CFArray that uses ARC for memory management, which is good. Our CGColors came from an Objective-C method call—so ARC is already managing their memory (see “ARC and Toll-Free Bridging” in Chapter 2 for more information).

As a result, this trick requires considerably less typing than creating a CFArray directly. We do need to use the __bridge annotation to tell ARC that we’re not transferring the references’ ownership, but other than that, memory management works as expected.

In our code, we create two colors. One is white with a 75 percent alpha. The other is completely transparent. We place these into an array and pass the array to the gradient. The CAGradientLayer then makes a smooth, linear transition that slowly fades out as you move down the screen. This adds a highlight to the top of our button.

We do the same thing for the bottomGradient. The only difference is that we increase its origin’s y value to position it on the bottom half of the button. We also use black colors whose alpha values will transition between 20 percent and completely clear. These will slightly darken the bottom half of our control.

Next, we set the corner radius, thus rounding the corners. We then clip our drawing to the area bounded by our rounded corners. This will clip our gradient sublevels as well.

Then we give our button a 2-pixel border, whose color matches the button’s title color. Notice, however, that whereas the button used a UIColor object, the CALayer uses CGColor structures. Again, we can request a CGColor reference from our UIColor object.

Run the application again. Now when you press the unit button, our modal view’s Done button looks all fancy (Figure 4.18).

Figure 4.18 Done button with rounded corners

image

This works fine in portrait mode, but if you rotate the view, you’ll notice that our gradients don’t stretch to fill the button. The problem is, we cannot automatically resize these layers. Instead, we need to manually resize them whenever the system lays out our views. To do this, implement the view controller’s viewDidLayoutSubviews method.

- (void)viewDidLayoutSubviews {
    CALayer* layer = self.doneButton.layer;
    CGFloat width = layer.bounds.size.width;
    for (CALayer* sublayer in layer.sublayers) {
        if ([sublayer.name hasSuffix:@"Gradient"]) {
            CGRect frame = sublayer.frame;
            frame.size.width = width;
            sublayer.frame = frame;
        }
    }
}

This method is called right after the system lays out all our subviews. Here, we just grab a reference to our doneButton’s layer and the layer’s width. We then iterate over all the sublayers. If the sublayer has a name that ends with “Gradient,” we resize it to match the button’s width. This way our custom layers will be resized, but we won’t alter any of the button’s other layers.

Try it out. Switch back and forth between the enter weight and unit select views. Rotate the interface to all the different orientations. If everything is working, commit all our changes. Next stop, the history view.


While we used the CALayer to give our button rounded corners, you could use the same technique for any UIView. One obvious example would be to create UIImageView objects with rounded corners and possibly even a glassy sheen. However, you may find it useful for any number of UIView objects: text views, Web views, scroll views, map views, or any of the custom views you may build.

While this approach is convenient, it can become computationally expensive. Performing these types of Core Animation manipulations require our views to be drawn to an offscreen buffer before being drawn onto the main view. This occurs on each and every frame both for our altered view and for any of its subviews.

This is particularly important in any view that expects smooth animation (for example, scroll views or table views). Having a number of objects performing offscreen rendering will quickly turn a smoothly scrolling interface into a jerking, stuttery mess.

We can use Instruments to detect views that are using offscreen rendering (see Bonus Chapter B for more information on Instruments). Connect your test iOS device, and set the scheme to run on it. Then select Product > Profile from the main menu. This will launch Instruments. When prompted, start the Core Animation template. Select the Core Animation track, and make sure the Color Offscreen-Rendered Yellow option is selected (Figure 4.19). This will highlight any views that are rendered offscreen in yellow, making them easy to spot.

Figure 4.19 Highlighting offscreen-rendered views using Instruments

image

As you can see, when you touch the units button, the entire screen is rendered offscreen as it flips. However, once the transition animation finishes, the rest can be rendered directly. Only our custom button continues to render offscreen (Figure 4.20).

Figure 4.20 All offscreen-rendered views are highlighted in yellow

image

If Core Animation becomes a performance issue, the easiest fix is to set the CALayer’s shouldRasterize property to YES. This tells Core Animation that the layer will require offscreen rendering and will force it to cache the offscreen image and reuse it whenever possible. In some cases, this can dramatically improve Core Animation’s performance.

However, shouldRasterize isn’t a magic “run faster” flag. It should only be used under very specific circumstances. Obviously, if our view’s appearance is changing, then caching doesn’t make sense. Core Animation will need to throw out the cache and re-render the view for each new frame. Worse yet, if the view wouldn’t have required an offscreen pass, setting shouldRasterize will force it to be rendered offscreen. This will actually reduce the app’s performance.

In Health Beat, shouldRasterize doesn’t help us at all. Our button’s final image is only rendered once, so we would never reuse the cache. Creating it only wastes memory. However, if we had placed our button in a scroll view, setting shouldRasterize might make a big difference.

If we cannot use shouldRasterize to solve our performance problems, we may need to revert back to custom drawing. In our case, we could create a subclass of UIButton and override the drawRect: method, manually drawing our curved rectangle and gradient fills. Typically, the drawing code is only called once, when the object is first placed onscreen. It is then cached in the view’s CALayer and can be scrolled, resized, or otherwise animated without requiring additional drawing. Of course, the initial drawing may be slightly slower than the Core Animation method calls, but we’re converting a per-frame cost into a one-time cost. In many cases, this produces significant performance improvements.

We’ll cover custom drawing in more depth in Chapter 5, “Drawing Custom Views.”


Showing Weight History

Now let’s shift over to our history view. Here, we display a list of all the entries in our WeightHistory. We will use a UITableView to present these entries, showing one entry per row. Let’s start by designing a custom cell for these rows.

Right-click the Views group and select New File > Objective-C Class. Make sure it is a subclass of UITableViewCell, and name it HistoryCell. Now open HistoryCell.h. Modify the header file as shown:

#import <UIKit/UIKit.h>
#import "WeightEntry.h"
@interface HistoryCell : UITableViewCell
@property (nonatomic, strong) IBOutlet UILabel* weightLabel;
@property (nonatomic, strong) IBOutlet UILabel* dateLabel;
- (void)configureWithWeightEntry:(WeightEntry*)entry
                    defaultUnits:(WeightUnit)unit;
@end

Unlike view controllers, outlets cannot be Control-dragged from the storyboard to a view’s header file. Instead, we need to manually declare them first. We’re also declaring a method to configure our cell—changing the contents of its labels based on the WeightEntry and WeightUnit arguments.

Switch to the implementation file. We’ll start by synthesizing our properties.

@synthesize weightLabel=_weightLabel;
@synthesize dateLabel=_dateLabel;

Then implement configureWithWeightEntry:defaultUnits:.

- (void)configureWithWeightEntry:(WeightEntry*)entry
                    defaultUnits:(WeightUnit)unit {
    self.weightLabel.text = [entry stringForWeightInUnit:unit];
    self.dateLabel.text =
      [NSDateFormatter
       localizedStringFromDate:entry.date
       dateStyle:NSDateFormatterShortStyle
       timeStyle:NSDateFormatterShortStyle];
}

Here, we use our WeightEntry’s stringForWeighInUnit: method to set the weightLabel outlet with a correctly formatted weight string. Next, we use NSDateFormatter to create a properly localized date string using the short date and short time formats. We then assign this to our dateLabel outlet.

Now, let’s open the storyboard and zoom in on our history view. Select the prototype cell and switch to the Identity inspector. Set the cell’s class to HistoryCell. Then switch back to the Attributes inspector. Make sure the Accessory attribute is set to Disclosure Indicator. This will add a gray chevron on the right side of our cell.

Xcode provides two built-in accessories with very similar functions. Both the Disclosure Indicator and the Detail Disclosure indicate that the application has additional information related to this row. However, there are slight differences, both in the way they operate and in their intended use.

Detail Disclosure creates a round blue button with a white chevron. When the user taps the button, it should navigate to a detail view for the selected item. The Disclosure Indicator, on the other hand, just provides the gray chevron image. Here, the user must select the row itself, and they are then navigated to a sublist (usually containing additional options).

Arguably, Health Beat should use Detail Disclosure accessories—but I feel that having users select the row, not the disclosure button, works better. Besides, our detail view is a list (of sorts), so it’s not wholly inappropriate, and even Apple isn’t 100 percent consistent with their accessories.

Now, drag out two labels. Set the first label’s title to Weight. Make it 100 points wide, and set the font to 18-pt System Bold. Then drag it until it is vertically centered along the cell’s left margin. In the Size inspector, lock its Autosizing position to the top and the left side.

Make the second label right-aligned 12-pt System Italic with a light gray text color. Set its title to Short Date and Time. Then align it with the Weight label, stretched so it fills the area between the Weight label and the Disclosure Indicator accessory. Its Autosizing position should be locked to the top and the right side (Figure 4.21).

Figure 4.21 The finished history cell

image

Now, right-click the prototype cell and draw the connections from the dateLabel outlet to our Short Date and Time label. Draw a second connection from the weightLabel outlet to our Weight label. With that, our cell prototype is ready, and we just need to finish the HistoryViewController.

Let’s start by importing our HistoryCell class at the top of HistoryViewController.m.

#import "HistoryCell.h"

Now we need to clean up our temporary code. Our history list should only have a single section, so numberOfSectionsInTableView: should still return 1. However, we need a number of rows equal to the number of entries in our weight history. Modify tableView:numberOfRowsInSection: to return this value.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // We only have a single section.
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView
   numberOfRowsInSection:(NSInteger)section
{
    // Return the number of entries in our weight history.
    return [self.weightHistory.weights count];
}

Next, navigate to the tableView:cellForRowAtIndexPath: method, and modify this method as shown.

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"History Cell";
    HistoryCell *cell =
        [tableView dequeueReusableCellWithIdentifier:
         CellIdentifier];
    // Configure the cell...
    WeightEntry* entry =
        [self.weightHistory.weights objectAtIndex:indexPath.row];
    [cell configureWithWeightEntry:entry
                      defaultUnits:self.weightHistory.defaultUnits];
    return cell;
}

It’s important to understand how table view cells work. We want to make our tables as efficient as possible, and there’s no point creating 10,000 separate cells if only ten of them can fit on the screen at a time. Therefore, when a cell scrolls off the screen, we should recycle it, reusing it the next time we need a new cell. Fortunately, UIKit can automatically do this for us.

To support cell reuse, we create a unique identifier for our cells. This is particularly important if you have different cells with different formats or even different classes. Each format needs its own identifier. When we need a new cell, we check to see if we have any available, unused cells by calling dequeueReusableCellWithIdentifier: and passing in our cell identifier.

Before iOS 5, if this method couldn’t find an unused cell, it simply returned nil. We then had to create a new instance ourselves. In the simplest cases, this was not too difficult; however, if our table had a number of different cell types, the code could rapidly grow complex. Fortunately, iOS 5 has automated much of this for us.

As long as we’re using a cell prototype from our storyboard, or a cell from a nib that we’ve registered using registerNib:forCellReuseIdentifier:, the dequeueReusableCellWithIdentifier: method will always returns a valid cell object. It will still reuse an existing cell, if possible; however, if nothing’s available, it will automatically create a new cell for us.

As you can see, this greatly simplifies our tableView:cellForRowAtIndexPath: method. When we modified this method, we deleted more code than we added.

Our code grabs a HistoryCell instance using our HistoryCellIdentifier constant. This, of course, matches the identifier set in our storyboard. Then we get the WeightEntry that corresponds with the current row, and we pass that weight entry and our default units value into the cell’s configureWithWeightEntry:defaultUnits: method. This, in turn, properly sets the text in the cell’s labels.

If you run the application now, you can enter new weight values; however, they may not appear in the history view. This is because our application doesn’t yet update the history view when our model changes. Let’s fix that.

Responding to Changes in the Model

Navigate back to the top of the file, and add the following extension before the @implementation block:

@interface HistoryViewController()
- (void)reloadTableData;
- (void)weightHistoryChanged:(NSDictionary*) change;
@end

This defines two private methods. The first, reloadTableData, will reload the entire table. We will call this whenever the default weight unit changes, since we will need to rewrite all the weight strings in all the cells.

The second method, weightHistoryChanged:, will be called whenever a WeightEntry is added to or removed from our history. In this case, we want to modify only the affected cells (adding or removing individual cells as needed).

Now, navigate to the viewDidLoad method and modify it as shown here:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Uncomment the following line to preserve
    // selection between presentations.
    // self.clearsSelectionOnViewWillAppear = NO;

    // Uncomment the following line to display an Edit button
    // in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;
    // Register to receive KVO messages when the weight history
    // changes.
    [self.weightHistory addObserver:self
                         forKeyPath:KVOWeightChangeKey
                            options:NSKeyValueObservingOptionNew
                            context:nil];
    // Register to receive messages when the default units change.
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(reloadTableData)
               name:WeightHistoryChangedDefaultUnitsNotification
             object:self.weightHistory];
}

The addObserver:forKeyPath:options:context: method registers our HistoryViewController as an observer of our weight history. Our controller will receive a notification whenever the list of weight entries changes. Notice that the KVOWeightChangeKey actually points to the private weightHistory property (see “The WeightHistory Class” in Chapter 3). We could observe the public weights property, but unfortunately, because it’s a virtual property we only receive a general notification that the array has changed—we don’t get any additional information about the change. When we observe the weightHistory array directly, we get additional information about the type of change and a list of the actual indexes that changed.

In many ways, using the KVOWeightChangeKey really lets us break the WeightHistory class’s encapsulation. In my opinion, this is not necessarily ideal, but by using a public variable for the key, we are essentially blessing this backdoor access. We are promising that we won’t change the underlying implementation without also changing KVOWeightChangeKey to match.

Realistically, however, we should change the WeightHistory code to manually throw the correct KVO notifications for the weights property and just get rid of the backdoor access. However, I wanted to show how to automatically generate KVO notifications using the keyPathsForValuesAffecting<key> method. This has the fortunate side effect of also highlighting some of the limits of this approach.

The addObserver:selector:name:object: method registers our controller to receive WeightHistoryChangedDefaultUnitsNotification messages from our model (and only from our model). When a matching notification is found, the notification center will call our reloadTableData method directly.

Next, we need to remove our observers when the view unloads. Modify the viewDidUnload method as shown here:

- (void)viewDidUnload
{
    [self.weightHistory removeObserver:self
                            forKeyPath:KVOWeightChangeKey];

    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super viewDidUnload];
}

This shows another difference between KVO and notifications. For KVO, we must remove each observer/key pair separately. For notifications, we have a convenience method that removes all notifications for a given observer.

Now we must respond to the notifications. We need to implement the observeValueForKeyPath:ofObject:change:context: method and our two private methods, reloadTableData and weightHistoryChanged:.

#pragma mark - Notification Methods
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context   {
    if ([keyPath isEqualToString:KVOWeightChangeKey]) {
        [self weightHistoryChanged:change];
    }
}

All KVO notifications call the observer’s observeValueForKeyPath:ofObject:change:context: method. Here, we simply check to ensure that the notification’s key path matches the KVOWeightChangeKey. If that is the case, we call the weightHistoryChanged: method, passing in the change dictionary. We could process the changes here, but I like to keep the ObserveValueForKeyPath:ofObject:change:context: method as clean and simple as possible, typically using it to dispatch out to other methods. After all, this method will grow increasingly complex if we start adding new KVO notifications.

- (void)weightHistoryChanged:(NSDictionary*) change {
    // First extract the kind of change.
    NSNumber* value = [change objectForKey:NSKeyValueChangeKindKey];
    // Next, get the indexes that changed.
    NSIndexSet* indexes =
        [change objectForKey:NSKeyValueChangeIndexesKey];
    NSMutableArray* indexPaths =
        [[NSMutableArray alloc] initWithCapacity:[indexes count]];
    // Use a block to process each index.
    [indexes enumerateIndexesUsingBlock:
        ^(NSUInteger indexValue, BOOL* stop) {
        NSIndexPath* indexPath =
            [NSIndexPath indexPathForRow:indexValue inSection:0];
        [indexPaths addObject:indexPath];
    }];
    // Now update the table.
    switch ([value intValue]) {
        case NSKeyValueChangeInsertion:
            // Insert the row.
            [self.tableView insertRowsAtIndexPaths:indexPaths
                withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSKeyValueChangeRemoval:
            // Delete the row.
            [self.tableView deleteRowsAtIndexPaths:indexPaths
                 withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSKeyValueChangeSetting:
            // Index values changed...just ignore.
            break;
        default:
            [NSException raise:NSInvalidArgumentException
                format:@"Change kind value %d not recognized",
                [value intValue]];
    }
}

The KVO change dictionary becomes particularly useful when we are monitoring collections. It contains information on both the type of change that occurred and the affected indexes. In our weightHistoryChanged method, we start by extracting the type of change. There are four possible types: inserts, removals, replacements, and the somewhat oddly named “setting” changes.

The first two should be obvious. You are adding or deleting one or more elements in the collection. Replacement merely means you are changing the value at a given index in the collection. Setting changes mean you are changing the value of the key path itself. Usually, this occurs when you change the property’s value. For a collection, that means replacing the current collection with an entirely new one.

Next, we extract the set of affected indexes. Notice that the change dictionary returns an NSIndexSet. However, we need an NSArray of NSIndexPaths. We therefore need to convert our indexes.

Here, we’re using a block to iterate over our index set. The enumerateIndexesUsingBlock: method takes each index in the index set and passes it to the provided block. The block should have two arguments: an NSUInteger representing the current index, and a pointer to a BOOL. The pointer is an output-only argument. Setting its value to YES will stop the enumerations, causing enumerateIndexesUsingBlock: to return.

Our block simply takes the index and converts it to an index path. When dealing with UITableViews, the index path contains both the row and the section of a particular entry. Our table has only one section, so we just hard-code the section index to 0. The block then adds the new NSIndexPath to our indexPaths array. Notice how our block can access and modify objects in the same lexical scope. For more information on blocks, see “Blocks” in Chapter 2.

Finally, we update the table. We are primarily concerned with insertions and removals. If either of these occurs, we modify the corresponding rows in the table. We should not get any replacement changes, but we could see an accidental setting change (for example, when the model’s history array is deallocated); however, we can safely ignore these. For anything else, we throw an exception.

As you can see, this method only modifies the table rows that actually changed. Additionally, we animate our changes using UITableViewRowAnimationAutomatic. This tells the system to automatically select an animation style that will look good, based on the type of table view and the cell’s location within the table. In general, you should use automatic animation unless you have an overriding reason to use something else. This helps maintain consistency across applications.

Unfortunately, the new rows are inserted while the table view is offscreen, so the animation will finish before we can navigate to the history view. However, you will get to see the removals once we add editing support.

Our last private method is simply a wrapper around the table view’s reloadData method.

- (void)reloadTableData {
    [self.tableView reloadData];
}

This raises the question, why don’t we have the table view observe the WeightHistoryChangedDefaultUnitsNotification and let the notification center call its reloadData method directly? While this would simplify our HistoryViewController class, it creates a subtle bug.

We still need to remove our table view from the observer list before it is deallocated. Unfortunately, our viewDidUnload method occurs after the view has already been released. Worse yet, if we accidentally try to access self.tableView in the viewDidUnload method, we will actually force the view to reload. Since viewDidUnload is only called during memory shortages, grabbing additional memory to rebuild our table view could cause our app to crash. At the very least, it would short-circuit our controller’s attempt to free up some unneeded memory.

Using the view controller as the observer lets us cleanly register and unregister ourselves for notifications, even if we do end up just dispatching the call back to the table.

OK, run the application again. Try adding a few dates. You should see them appear in the history list, with the most recent weight at the top. Try switching from pounds to kilograms and back. The history view should update automatically (Figure 4.22).

Figure 4.22 Automatically updating when the default units change

image

Editing the History View

Now that we can add new weights, we really need a way to remove them. The easiest option is to enable editing in the table view. To do this, we just need to add our controller’s edit button to the navigation bar. The code is already located in viewDidLoad. We just need to uncomment it. While we’re at it, let’s delete the rest of the comments.

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.navigationItem.rightBarButtonItem = self.editButtonItem;
    // Register to receive KVO messages when the weight history
    // changes.
    [self.weightHistory addObserver:self
                         forKeyPath:KVOWeightChangeKey
                            options:NSKeyValueObservingOptionNew
                            context:nil];
    // Register to receive messages when the default units change.
    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(reloadTableData)
               name:WeightHistoryChangedDefaultUnitsNotification
             object:self.weightHistory];
}

Pressing the edit button puts the table view in editing mode. By default, this displays a Delete icon beside each row. You can modify this behavior using several UITableViewDelegate methods, but for our case, the default behavior is exactly what we want.

Go ahead and run the app now. Enter a few weights, and then put the history view in edit mode. If you press one of the Delete icons, it will bring up a red confirmation button. However, pressing the confirmation button doesn’t do anything (Figure 4.23).

Figure 4.23 Editing the table

image

We still need to respond to the edit commands by both removing the row from the table and removing the corresponding WeightEntry from our model. To do this, uncomment and modify the tableView:commitEditingStyle:forRowAtIndexPath: method.

- (void)tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
    forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        [self.weightHistory removeWeightAtIndex:indexPath.row];
    }
}

Here we just verify that we are deleting an object. Then we remove the selected object from our weightHistory. That’s it. We’ve already built in support for removing table rows when entries are deleted from our model. Our existing notifications will trigger that code automatically. Additionally, we never add new entries from within the table view, so we don’t need to check for inserts here.

And that’s it. Build and run the application. Try adding and deleting weights. If everything works correctly, remember to commit your changes.

There’s a lot more to editing table views, of course. If you really want, you can add the ability to reorder the weights (though I don’t know why we would want that feature for this project). You could also add new weights directly from the table view. You can even allow multiple selections using UITableView’s allowsMultipleSelections and allowsMultipleSelectionsDuringEditing properties. However, I’ll leave those as homework.

Showing Detail Views

There is still one piece left to implement. When the user selects one of the weights, we want to display detailed information about our weight entry. Unfortunately, once you’ve listed the weight and the date, there’s really not much more you can say about a single WeightEntry, so we will fill out our detail view by comparing the selected weight entry with the minimum, maximum, and current monthly average from our WeightHistory.

We will also look at using static tables to organize and display our data. Static tables are a very convenient way to display a fixed set of information. We lose a little bit of flexibility in that we cannot change the number, order, or types of rows in our table. However, we can build our entire interface directly in the storyboard. We don’t even need to implement a data source for our table.

Designing the Static Table View

Open the storyboard and zoom in on the detail view scene. First things first, let’s turn it into a static table. Select the table view, then set its Content attribute to Static Cells. We also want to change the Style attribute to Grouped. This should give us a rounded bubble of three blank cells against a striped gray background (Figure 4.24).

Figure 4.24 After converting to a grouped static table

image

Now select and delete all but one of these cells. Select the remaining cell and change its Selection attribute to None. Then, drag out a label and place it in the row. Set its font size to 16pts, center it vertically, and align it with the left margin. Change its text to Monthly Average, and right-align the text. Finally, make sure the cell’s Autosizing settings are locked to the top and the left side.

Next, drag out a text field and position it so it’s centered vertically. Stretch it so it fills the space between our label and the row’s right margin. Its Autosizing settings should be locked to the left, right, and top, with horizontal scaling enabled. Finally, deselect the text field’s Enabled attribute. This will make the text field readonly. We can use it to display text, but the users won’t be able to edit it (Figure 4.25).

Figure 4.25 Our initial cell design

image

There’s a nice benefit to creating cells in our storyboard. Once we’ve designed one, we can copy it to create the others. This is a very easy way to create a number of controls, while keeping them all the same size and properly aligned.

Select the Table View section, and then change its Rows attribute to 2. We should now have two identical rows grouped together. While we’re here, change the Header attribute to Weight Entry.

Now select the table view itself. Change the Sections attribute to 2. This gives us two groups of two rows each. Notice that this copies everything, including the header. To modify the bottom section, select it and change its header to Statistics. Change its Rows attribute to 3. Now go through each row and set the label text. Starting at the top, they should be Weight, Date, Monthly Average, Loss from Max, and Gain from Min. When you’re done, the interface should match Figure 4.26.

Figure 4.26 Finished detail view interface

image

Now let’s create outlets for all our text views. Open the Assistant editor and make sure the DetailViewController.h file is showing. Control-drag from each text field to the header file. Name the outlets weightTextField, dateTextField, averageTextField, lossTextField, and gainTextField, respectively.

Now, switch back to the Standard editor and open DetailViewController.m. First things first, we no longer need a data source. Delete the following methods: numberOfSectionsInTableView:, tableView:numberOfRowsInSection:, and tableView:cellForRowAtIndexPath:.

Finally, we need to fill in our text fields with actual data. We will do this in the viewWillAppear: method. This lets us update the displayed values each time the view appears. However, this method gets a bit long, so let’s examine it in chunks.

- (void)viewWillAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    WeightUnit unit = self.weightHistory.defaultUnits;
    WeightEntry* currentEntry =
        [self.weightHistory.weights objectAtIndex:
        self.selectedIndex];
    CGFloat weight = [currentEntry weightInUnit:unit];
    // If the entry is within the same month.
    NSDate* startOfMonth;
    NSTimeInterval monthLength;
    [[NSCalendar currentCalendar] rangeOfUnit:NSMonthCalendarUnit
                                    startDate:&startOfMonth
                                     interval:&monthLength
                                      forDate:currentEntry.date];

First, we get some basic values. We grab the default unit value and the WeightEntry for the currently selected index. Then we extract the weight from our entry for the given unit.

Once we have the basic data, we want to calculate the start and length of the current month. We do that using the NSCalendar rangeOfUnit:startDate:interval:forDate: method. Notice that the startDate and interval are output-only arguments. You select a date and a calendar unit, and the method calculates the start and interval values for you.

CGFloat minWeight = CGFLOAT_MAX;
CGFloat maxWeight = CGFLOAT_MIN;
int monthlyCount = 0;
CGFloat monthlyTotal = 0.0f;
for (WeightEntry* entry in self.weightHistory.weights) {
    CGFloat sampleWeight = [entry weightInUnit:unit];
    if (sampleWeight < minWeight) minWeight = sampleWeight;
    if (sampleWeight > maxWeight) maxWeight = sampleWeight;
    // Check to see if it's in the same month.
    NSTimeInterval timeFromStartOfMonth =
        [entry.date timeIntervalSinceDate:startOfMonth];
    if (timeFromStartOfMonth > 0 &&
        timeFromStartOfMonth <= monthLength) {
        monthlyTotal += sampleWeight;
        monthlyCount++;
    }
}
CGFloat monthlyAverage = monthlyTotal / (float)monthlyCount;

Next, we iterate over the weight history, looking for the minimum and maximum weight values. We also use the timeIntervalSinceDate: method to calculate the number of seconds between our startOfMonth date and the given WeightEntry’s date. If this is a positive number (meaning the weight entry occurred after the month began) and it is less than our monthLength interval, then it falls within the month in question. We add the weight to our monthly total, and we increment our monthly count.

Once we have iterated over the entire history list, we calculate the monthly average from our total and count.


    // Now fill in our values.
    self.weightTextField.text =
        [WeightEntry stringForWeightInLbs:weight inUnit:unit];
    if (weight < monthlyAverage) {
        self.weightTextField.textColor = [UIColor colorWithRed:0.0f
                                                         green:0.5f
                                                          blue:0.0f
                                                         alpha:1.0f];
    }
    if (weight > monthlyAverage) {
        self.weightTextField.textColor = [UIColor colorWithRed:0.5f
                                                         green:0.0f
                                                          blue:0.0f
                                                         alpha:1.0f];
    }
    self.dateTextField.text =
    [NSDateFormatter
     localizedStringFromDate:currentEntry.date
     dateStyle:NSDateFormatterShortStyle
     timeStyle:NSDateFormatterShortStyle];
    self.averageTextField.text =
        [WeightEntry stringForWeightInLbs:monthlyAverage
                                   inUnit:unit];
    self.lossTextField.text =
        [WeightEntry stringForWeightInLbs:maxWeight -
            weight inUnit:unit];
    self.gainTextField.text =
        [WeightEntry stringForWeightInLbs:weight -
            minWeight inUnit:unit];
}

Here we fill in our text fields. Again, we use the stringForWeightInLbs:inUnit: class method to generate properly formatted and localized weight strings. The only catch here is the weightTextField. If the current WeightEntry’s weight is less than the monthly average, we set the text color to dark green. If it is greater than the monthly average, we set it to dark red. Here, we are using custom colors by specifying their RGBA values (Figure 4.27).

Figure 4.27 Filling in the detail view

image

There’s one last step. Modify shouldAutorotateToInterfaceOrientation: so that it returns YES, allowing us to rotate into any orientation.

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


There are a number of different ways we could display textual output in iOS. We have already seen two: UILabels and UITextFields. These are useful when you’re displaying relatively short pieces of text in a more-or-less static layout. The UITextView gives us a little more flexibility, letting us enter multi-line blocks of text. Like the others, we can set the font type, size, and text color. However, we must use the same settings for the entire text view.

What if we want more-complex text? Something with headers and body text? Something that uses bolds, italics, and different colors to draw and focus attention? After all, the Mac has historically had great text-layout capabilities. In the ’90s, the Mac was at the forefront of the desktop publishing revolution. Therefore, you would expect iOS would have inherited some of this tradition. Unfortunately, there seems to be an odd gap in the technology here.

There are a number of different technologies, each with their own strengths and weaknesses. Some, like NSString’s UIStringDrawing category, provide useful methods for drawing strings onto the screen—but like the UILabels and UITextFields, these are really only suited for short labels. The Core Animation CATextLayer and Core Graphics text support also fall into this category—useful for some corner cases, but not a general solution.

To do real formatted text, we have two main options: use a UIWebView or build it ourselves using the Core Text framework.

The UIWebView represents the high-level approach. Here, we lay out our text using HTML and CSS. We then load this into the Web view to display it. This has a number of advantages. You get full copy-paste support for free. You can also leverage any existing Web development experience you might have. You can incorporate images and other media. The UIWebView supports a wide range of (non-Flash) Web technologies, including JavaScript. You can even communicate between your native Objective-C code and the JavaScript running in your Web view, producing a hybrid application. In fact on iOS 5, with a little help from contentsEditable and execCommand, you can easily build a fully featured rich text editor.

The main disadvantage is that the UIWebView is an infamous memory hog. And it’s true; the system performs a lot of behind-the-scenes setup when you instantiate your first Web view. This can cause a noticeable lag when first displaying a Web view. However, this reputation may be a little unfair. You can often hide the initial startup cost by pre-loading your Web view, and once the initial setup is done, the performance really isn’t as bad as most people think.

Core Text, on the other hand, provides a full-throttled, high-octane, pedal-to-the-metal text system. It is designed for high performance and ease of use. It can also provide higher quality typographical controls—especially on older devices (kerning and ligatures were not available in the UIWebView before iOS 5).

Core Text is particularly well suited for paging apps, where the Web view more naturally supports scrolling apps. Core Text is also tightly integrated with Core Graphics, giving us the power to perform a wide range of drawing tasks.

On the downside, because it’s a low-level library you will have to do most of the work yourself. There’s no support for other media, and there’s no support for copy-paste. This last point is particularly important. The iOS copy-paste system acts as a gateway to a lot of really interesting controls. As developers, we can add our own items into the copy-paste menu, which lets us include support for everything from highlighting the selected text to looking up keywords in Wikipedia.

My basic advice is to always use a UIWebView when you can. They can be frustrating at times, but you get so much bang for your buck. Only consider switching to Core Text when you absolutely need the additional power.

You can find more information on both of these technologies in Apple’s documentation. See the Text Web and Editing Programming Guide for iOS, and the Core Text Programming Guide. You can also find some execCommand documentation (for editing rich text in a Web view) at https://developer.mozilla.org/en/rich-text_editing_in_mozilla.


Wrapping Up

That’s it. Run the application. Try adding a few weight entries. Move to the history view and then select a few entries from the list. Rotate both the history view and the detail view to make sure they work in all orientations. If everything’s working fine, commit your changes.

That finishes off two of our three views. In the next chapter, we will look at creating a custom view for our GraphViewController. In Chapter 6, we will learn how to save both our application date and our user defaults.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.214.194