TweetFollow Us on Twitter

iPhone Productivity Applications, Part II

Volume Number: 25 (2009)
Issue Number: 02
Column Tag: iPhone

iPhone Productivity Applications, Part II

Developing applications that manage complex data

by Rich Warren

(Ed note: Parts I and II were actually printed out of order. Click here for Part I.)

The Project So Far...

In the last article, we began building the GasTracker application for the iPhone. This application let us record the amount of gas, cost and odometer reading each time we fill up our car. It then calculates and display useful statistics, like average MPG or average cost per day.

We started by building the application's skeleton, the tab view and the model. By the end of the article, the application should have compiled without any errors or warnings. When you run the app, you can switch from tab to tab or customize the tab bar. Of course, the views didn't do anything yet. Still, it was a good start.

In this article, we will continue to flesh out GasTracker. We will focus on setting up the navigation controller and our table views. We will also add a view for entering data, and create custom classes for each of the stats views. Once that's done, we'll have a fully functional productivity application.

Most of this article will focus on the history view. This view is based on the 1-2 punch of a table view backed buy a navigation controller. This is a common design for iPhone applications, especially productivity apps.

The navigation controller manages a stack of view controllers. You push new view controllers onto the top of the stack. You can also pop unwanted controllers off the top of the stack. The topmost controller is active, and the navigation controller displays its view. All other views on the stack remain hidden.

We start with a table view on the top of the stack. The table view displays all the entries we made while purchasing gas. When we tap on a row, we want to move to a detailed view for that row. We create a new view controller for the detailed view, and push it onto the stack. To get back to the main table, we just pop the controller off the stack. In this way, the table/navigation control combo let us navigate through our (albeit short) data hierarchy.

We will examine the interactions between the navigation controller and the table view in more detail later on, but before we can go any further, the user needs some way to add entries to the model.

Add Entry View

Let's start with the nib. Right click on the Resources folder and select Add ... New File.... From the User Interfaces templates, select View XIB. Name it AddEntryView.xib and click Finish.

Now open the nib in Interface Builder. We want to add four labels, three text fields and a single button. Organize the controls to match the image below. Drag the control from the Library Window, and position it on the view. As you move it around the view, notice that blue guidelines appear when you get close to the edges or to other controls, use these to help position the controls correctly.


AddEntryView.xib

To change the label text, either double click on the label and edit it directly, or select the label and change the Text attribute in the Attributes Inspector. For the Today's Date label, stretch the label so that it fills the view from margin to margin, then center align the text. The alignment controls can also be found on the Attributes Inspector.

The button's text works the same way, except it is called the Title attribute, and the text is already centered by default.

In general, I find it easiest to place the text fields first. Make sure they are aligned along the view's right margin with the proper vertical spacing. Then place the labels relative to the text fields. Once these are set, adjust the text field width based on the longest label, then adjust the others to match.

Interface Builder has a number of layout tools to help you. Check out Apple's Interface Builder User's Guide, for more information than you will probably ever need.

Now we need a controller for this view. Go back to XCode, and again right click on the Classes group in the Groups & Files tree, and select Add ... New File.... From the Cocoa Touch Classes templates, select UIViewController subclass. Name the class AddEntryViewController.m, and click on the Finish button.

Now, open AddEntryViewController.h and edit it as follows:

AddEntryViewController.h

#import <UIKit/UIKit.h>
@class Model;
@interface AddEntryViewController : UIViewController<UITextFieldDelegate> {
   
   IBOutlet UITextField* totalCost;
   IBOutlet UITextField* amountOfGas;
   IBOutlet UITextField* odometer;
   IBOutlet UILabel* todaysDate;
   IBOutlet UIButton* doneButton;
   
   Model* model;
   
}
@property (nonatomic, assign) Model* model;
-(IBAction)done;
@end

Here, we define the outlets for our text fields, the Today's Date label and our button. We also create a property for our Model, and create an action for our button. Nothing too surprising.

However, before we move on to the implementation file, let's take a quick sidestep. We're going to spend a lot of time working with formatted strings. We want to use basically the same formatting and parsing methods throughout this project, in a number of different classes.

In many languages (I'm looking at you, Java), we would create a utility class to hold these common methods, but Objective-C has a better solution. We can create a category for an existing class. Categories let us add methods to existing classes. We don't even need the original class's source code.

So, let's create a category that will add our specialized formatting/parsing methods directly to NSString. Add a new file to your Classes group. Since there's no Category template, just add an NSObject subclass. Name the file Formatter.m.

Now, open Formatter.h. Edit it as shown below:

Formatter.h

#import <UIKit/UIKit.h>
@interface NSString (Formatter) 
+ (NSString*)shortDate:(NSDate*)date;
+ (NSString*)longDate:(NSDate*)date;
+ (NSString*)decimal:(double)value;
+ (NSString*)currency:(double)value;
+ (double) parseDecimal:(NSString*)string;
+ (double) parseCurrency:(NSString*)string;
   
@end

This creates a category on NSString named Formatter. It then declares two methods that create formatted strings from an NSDate object. There are two additional methods to create formatted strings from doubles, and two methods that parse a properly formatted NSStrings, producing double values.

Let's define these methods in Formatter.m. Functionally, these are all very similar. I will show you a single formatter and its corresponding parser below. I leave the rest up to you.

Formatter.m

#import "Formatter.h"
@implementation NSString (Formatter) 
+ (NSString*)decimal:(double)value{
   
    NSNumber *number = [NSNumber numberWithDouble:value];
    NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
    [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
   
    NSString *decimalString = [formatter stringFromNumber:number];
    [formatter release];
   
    return decimalString;
}
+ (double) parseDecimal:(NSString*)string {
    NSNumberFormatter* formatter = [[NSNumberFormatter alloc] init];
    [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
   
    NSNumber* value;
    NSString* error;
   
    [formatter getObjectValue: &value forString:string 
        errorDescription:&error];
   
    [formatter release];
   
    return [value doubleValue];
}
@end

In the decimal: method, we create an NSNumberFormatter, and set its style to NSNumberFormatterDecimalStyle. By default, all NSFormatter classes set a number of attributes based on our local. In my case, it sets the decimal separator to a period ".", and the thousands separator to a comma ",". For currency formatted numbers, it would also set the currency symbol to the dollar sign "$".

While you can further modify the formatter's behavior, the default behavior will work fine for us. Simply call the NSNumberFormatter's stringFromNumber: method, and return the resulting string.

The parser also uses NSNumberFormatter. As before, we create our formatter and set the desired style. Then we call getObjectValue:forString:errorDescription: and return the value.

Note: we completely ignore the error message here. Generally speaking, that's a bad idea. However, since we are using the same formatter to both create and parse the strings, there shouldn't be any unexpected errors. So, maybe it's ok to let it slide, just this once.

OK, back to the AddEntryViewController. Let's open the implementation file. First things first, let's import all the header files we'll need for this code. Also, lets define a few private methods. These are methods that our class will use internally, but that cannot (or at least, cannot without some difficulty and hacking) be called from the outside.

AddEntryViewController.m Imports and Private Methods

#import "AddEntryViewController.h"
#import "Formatter.h"
#import "Entry.h"
#import "Model.h"
// Define private methods
@interface AddEntryViewController()
-(double)getTotalCost;
-(double)getAmountOfGas;
-(double)getOdometer;
-(void)validateControls;
@end

Note: we are using an extension to define our private methods. Extensions look like categories, but without the name. They also operate similarly to categories; both let us add methods to existing classes. But, extensions work in a much more constrained way.

When you create an extension, the method definitions must appear in the class's main @implementation block. Essentially, they provide a compiler-checked method for declaring an API outside the main @interface block. They are most often used to define private methods.

Now let's look at the implementation. First we synthesize our model property. Then we define our done action. The done action creates a new entry based on the values from the view, adds the entry to the model, and then pops the AddEntryView from the navigation controller.

Popping a view from the navigation controller dismisses the view and returns us to the next view on the stack. Since we animated the transaction, the AddEntryView will slide off the right side of the screen, and the previous view will slide back in from the left.

AddEntryViewController.m Implementation

@implementation AddEntryViewController
@synthesize model;
#pragma mark Actions
-(IBAction)done {
   
    NSDate* date = [NSDate date];
    Entry* entry = [[Entry alloc] initWithTotalCost: [self getTotalCost]
                                      amountOfGas: [self getAmountOfGas]
                                            odometer: [self getOdometer]
                                                           onDate: date];
   
    [model addEntry:entry];
    [self.navigationController popViewControllerAnimated:YES];
    [entry release];
}

Now let's look at methods that override existing methods from UIViewController, its superclasses or the UITextFieldDelegate protocol. viewDidLoad is automatically called when a view is loaded from a nib file. At this point, the IBOutlet values are valid. We can therefore use this method to do additional initialization on any objects managed by the nib. In our case, we want to set todaysDate to a string corresponding to the current date. We then make sure the totalCost text field is selected, by calling becomeFirstResponder. This, in turn, brings up the keyboard.

viewDidLoad Method

#pragma mark Polymorphic Methods
- (void)viewDidLoad {
    [super viewDidLoad];
   
   NSDate* date = [NSDate date];
   todaysDate.text = [NSString longDate:date];
   
   [totalCost becomeFirstResponder];
}

Next we override shouldAutorotateToInter-faceOrientation:. Despite its long name, this is actually a simple method. If it returns YES, the view will automatically rotate when the user changes the iPhone's orientation. Returning NO prevents autorotation.

Of course, nothing is ever as simple as it first seems. Views inside a tab bar will not autorotate unless all the view controllers contained by the tab bar also return YES. Even a single NO will veto rotation for all views. Since our navigation controller is managed by the tab bar controller, this includes any controllers pushed onto the navigation controller - even if they're not currently visible.

shouldAutorotateToInterfaceOrientation Method

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

Next, we simply keep the default stub for didReceiveMemoryWarning. This just calls the super class. We could use this method to free up view-specific memory, but for this project we will leave the method untouched.

didReceiveMemoryWarning Method

- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview
    [super didReceiveMemoryWarning]; 
    // Release anything that's not essential, such as cached data
}

Our controller also acts as a delegate for the text field. textFieldDidEndEditing: is automatically called whenever a text field loses first responder status. Here, we determine the numeric value for the text field, then format the number using either the currency or the decimal format, as appropriate.

Actual formatting is done using the currency: and decimal: methods defined in our NSString category. Note: if the user enters an invalid value, the field is set to 0.0. After formatting the text field, we validate the current value of all the fields. We'll look more closely at the validateControls method later.

The second delegate method, textField-ShouldReturn:, is called automatically whenever the user taps the return button. In our case, the text field simply gives up its first responder status. This dismisses the keyboard.

textFieldDidEndEditing: Method

 
- (void)textFieldDidEndEditing:(UITextField *)textField {
   
   double value = 0.0;
   NSString* text = textField.text;
   
   if ([text length] > 0) {
      value = [text doubleValue];
   }
   
   if (textField == totalCost) {
      textField.text = [NSString currency:value];
   }
   else {
      textField.text = [NSString decimal:value];
   }
   
   [self validateControls];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
   
   [textField resignFirstResponder];
   return NO;
}

Finally, we have our dealloc and private methods. dealloc simply releases all the IBOutlets. Note: the memory retention rules for nib-created objects are somewhat different on the iPhone than on Mac OS X. Here, objects are created with a retain count of 1 and then autoreleased. Views retain their subviews. Additionally, when an object is assigned to an IBOutlet, the value is set using the setValue:forKey: method. This method calls the appropriate setter, if available. If no setter method can be found, it sets the variable directly, and retains the object.

We haven't defined a setter for our outlets. As described above, the objects are automatically retained, and it is our responsibility to release them when we're done. If you want more details on nib object memory management, check out The Nib Object Life Cycle section of Apple's Resource Programming Guide.

Next we have our private methods. Since we declared these methods in an extension, we must implement them in our class's main implementation block. Most of these methods simply use the currency and decimal parsing methods from our NSString category. validateControls simply verifies that all the text fields have a valid value greater than 0.0. Once all the fields have valid entries, it enables and highlights the done button.

dealloc and private methods

#pragma mark Dealloc
- (void)dealloc {
    [totalCost release];
    [amountOfGas release];
    [odometer release];
    [todaysDate release];
    [doneButton release];
   
    [super dealloc];
}
#pragma mark Private Methods
-(double)getTotalCost {
   
    return [NSString parseCurrency:totalCost.text];
}
-(double)getAmountOfGas {
   
    return [NSString parseDecimal:amountOfGas.text];
}
-(double)getOdometer {
   
    return [NSString parseDecimal:odometer.text];
}
-(void)validateControls {
   
    BOOL enable = [totalCost.text length] > 0;
    enable &= [amountOfGas.text length] > 0;
    enable &= [self getTotalCost] > 0.0;
    enable &= [self getAmountOfGas] > 0.0;
    enable &= [self getOdometer] > 0.0;
   
    doneButton.enabled = enable;
    doneButton.highlighted = enable;
   
}
@end

Now, go back to Interface Builder and make all the necessary connections. Make sure the File's Owner's class is set to AddEntryViewController. Connect the File's Owner's view outlet to the view object. Then connect totalCost, amountOfGas, odometer, todaysDate and the done outlets to the corresponding controls. Set the File's Owner as the delegate for each of the text field, and set the File's Owner's done action to the button's Touch Up Inside event. Oh, one last thing. Make sure the button's Autosizing has a strut set on the left and bottom, and no strut set at the top. This will keep the button positioned relative to the bottom of the view, which prevents it from being hidden behind the tab bar.

So far, so good. But we need a way to launch our control. Open MainWindow.xib and click on the History tab. Drag a Bar Button Item to the left side of the Navigation Bar. In the Attributes Inspector, change the button's Identifier to Add. It should match the picture below.


Now add the following function to the HistoryNavigationController. Don't forget to declare the method.

HistoryNavigationController's addEntry Method

-(IBAction)addEntry {
    AddEntryViewController* subview = 
        [[AddEntryViewController alloc] initWithNibName:@"AddEntryView" 
        bundle: nil];
   
    subview.model = self.model;
    [self pushViewController:subview animated: YES];
   
    [subview release];
}

This simply creates the AddEntryViewController using the AddEntryView.xib file. We set the controller's model property, and then push the controller onto our navigation controller. As described earlier, pushing a view controller onto the navigation controller makes it the current active view. Since we are animating the transition, AddEntryView will slide in from the right, while the main history view table slides off the left.

Now, link the button and action. You can access the HistoryNavigationController by single clicking on the History tab bar. If you end up with the Tab Bar Item selected, simply click another tab, then single click History again. Now right click the history tab and draw the connection between the addEntry action and the button.

Congratulations, you can now add new entries. You can't view them yet, but it's still progress. Next we look at the history view.

History View

We want to display all of our entries in a table view. To do this, create a UIViewController subclass named HistoryViewController. Edit HistoryViewController.h as shown below:

HistoryViewController.h

#import <UIKit/UIKit.h>
@class Model;
@interface HistoryViewController : UITableViewController 
    <UITableViewDelegate, UITableViewDataSource> {
    IBOutlet Model* model;
}
@property (nonatomic, retain) Model *model;
@end

Note, we change the superclass to UITableViewController. We also adopt both the UITableViewDelegate and the UITableViewDatasource protocols. As we will soon see, these methods are used to fill, format and control our table's behavior.

Note: The view controller, table view delegate and table view data source do not have to be the same class. In some cases, you may want to separate out some of these responsibilities to other classes. However, I often find it convenient to keep them together.

OK, now lets look at the implementation:

HistoryViewController.m imports and properties

#import "HistoryViewController.h"
#import "EntryViewController.h"
#import "Formatter.h"
#import "Model.h"
#import "Entry.h"
@implementation HistoryViewController
@synthesize model;

Nothing too surprising here. We import the header files and synthesize our model. The next two methods start the real work. viewDidLoad adds an edit button to the right side of our navigation bar. Remember, there's not a lot of space on the navigation bar. We've already added a button to create new entries on the left side, and the title is displayed in the middle.

The table view will automatically load our data the first time the table is displayed. However, the function viewDidAppear: gets called each time the view becomes active. By explicitly reloading the data here, we update the table view as the application transitions from the AddEntryView back to the HistoryView.

Of course, this may not be the most efficient approach. Strictly speaking, we only need to update the new row. For our purposes, the brute force approach works well enough, but if you want a more fine grained solution, UITableView has methods for adding and deleting single rows. It also has methods for batching updates. Check out insertRowsAtIndexPaths:withRowAnimation:, deleteRowsAtIndexPaths:withRowAnimation:, beginUpdates and endUpdates for more information.

viewDidLoad and viewWillAppear:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)viewWillAppear:(BOOL)animated {
   [self.tableView reloadData];
}

Now we get to the heart of it. The table view automatically calls the delegate and data source methods to layout its appearance and content. Most of these methods are optional, we simply implement the ones we need. Let's start with the simplest.

numberOfSectionsInTableView:
and tableView:numberOfRowsInSection:

#pragma mark Table Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView 
    numberOfRowsInSection:(NSInteger)section {
        return [model numberOfEntries];
}

These methods tell the table view that we have only a single section in our table, and that we want one row for each entry in our model.

On the iPhone, tables can only have one column; however the rows can be grouped into multiple sections. Each section can have its own header and footer. If you're using a grouped style table, you can even define custom UIViews (e.g. UILabels or UIImageViews) for the header and footer.

Next we return an appropriately formatted cell for each row.

tableView:cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease]; } int row = indexPath.row; Entry* entry = [model getEntryForIndex: row]; NSString* dateString = [NSString shortDate:entry.date]; NSString* costString = [NSString currency:entry.totalCost]; cell.text = [NSString stringWithFormat: @"%@: \t%@", dateString, costString]; cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton; return cell; }

First we try to reuse our cell. For efficiency, as long as the general format of the cell remains the same, you should reuse cells. Our cells are simple. We merely change the text for each row. So, we can safely reuse our cells. If we cannot reuse the cell (for example, if it doesn't exist yet) we create a new one.

Note: the table will automatically position and size the cell; in most cases you can simply pass CGRectZero in for the frame. If you have more complex cells (for example, cells with multiple subviews, each having their own autoresizing mask) you may need to use a non-zero frame size to make sure everything gets positioned properly. Otherwise, stick to CGRectZero.

Once we have a cell, we simply get the corresponding entry based on the row number. We then create a string from the entry's date and cost, and use that to set the cell's text property.

Finally, we add an accessory disclosure button to the row. This is a round, blue button with a white chevron displayed at the right edge of the row. The iPhone SDK provides a number of accessory buttons, each with a specific intended meaning. The accessory disclosure button should be used whenever selecting the row displays detailed information about the selected item.

We have already added an edit button. This allows the user to delete rows. We must make sure our delegate catches and handles these deletions.

tableView:commitEditingStyle:forRowAtIndexPath:

- (void)tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
      forRowAtIndexPath:(NSIndexPath *)indexPath {
   
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        int row = indexPath.row;
        [model removeEntryAtIndex:row];
      
        [self.tableView deleteRowsAtIndexPaths:
            [NSArray arrayWithObject:indexPath] 
             withRowAnimation:UITableViewRowAnimationFade];
   }
}

This method is called whenever the user edits the table. Here we check to make sure we're deleting a row. Then we remove the corresponding entry from our model, and delete the row with a fade animation.

We also want to display a detailed view of the entry whenever the user selects a row.

tableView:didSelectRowAtIndexPath:

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   
    int row = indexPath.row;
    Entry *entry = [model getEntryForIndex:row];
   
    EntryViewController *controller = [[EntryViewController alloc]
        initWithNibName:@"EntryView" entry:entry];
    [self.navigationController pushViewController:controller 
        animated:YES];
   
    [controller release];
}

Here, we get the corresponding entry from our model, create an EntryViewController using the EntryView nib and our entry object, then push that view onto the navigation controller. This will cause the new view to slide in from the right.

Of course, for this to work, we need an EntryViewController class and an EntryView.xib file. I will leave those as an exercise for the reader (or, if you want to cheat, you can download the complete source code from ftp. mactech.com).

OK, last and (let's be honest here) least, we have a few minor methods to round out our class.

Autorotation, memory warnings and dealloc

- (BOOL)shouldAutorotateToInterfaceOrientation:
    (UIInterfaceOrientation)interfaceOrientation {
    return YES;
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning]; 
    // Releases the view if it doesn't have a superview
    // Release anything that's not essential, such as cached data
}
- (void)dealloc {
    [model release];
    [super dealloc];
}
@end

You should be familiar with shouldAutorotate-ToInterfaceOrientation: by now. Remember, when you have views inside a tab bar controller, it's an all or nothing. Unless all the views return YES, none of them are allowed to autorotate.

Next, we have the default stub for our memory warnings. We don't have any non-essential data that we could release, so we simply call the super class's implementation.

Our dealloc method simply releases our model. No surprises there.

Now we need a nib. Create a new file named HistoryView.xib, using the View XIB template. In Interface Builder, set the File's Owner's class to HistoryViewController. Delete the View, and replace it with a Table View. Finally, draw a connection from the File's Owner's view property to our Table View object.

Now, back in MainWindow.xib, single click on the history tab then single click on the view. The Attributes Inspector should display View Controller Attributes. Set the NIB Name to History View. Change the class to HistoryViewController. Now right click on the view, and connect the model outlet to our model object.

That's it, the history view is done. Build and run your application. You should now be able to add, view and delete entries.

Stats Views

By comparison, the various stats views are simple. We'll do one together, just so you get the idea. Let's start by adding our outlets and setters to the StatsViewController. Open the header file and modify it as shown below:

StatsViewController.h

#import <UIKit/UIKit.h>
@class Model;
@interface StatsViewController : UIViewController {
   IBOutlet UILabel* titleLabel;
   IBOutlet UILabel* valueLabel;
   IBOutlet Model *model;
}
@property (nonatomic, retain) Model *model;
       - (void)setTitle:(NSString*)title;
 - (void)setDecimalValue:(double)value;
 - (void)setCurrencyValue:(double)value;
@end

If this were a production application, we would probably want artistic graphs that display stock-ticker-like history lines that show our MPG and costs changing over time. While Cocoa Touch's Quartz library makes it easy to create beautiful 2D drawings, those are beyond the scope of this article. Instead, we'll simply use UILabels to display overall averages. The titleLabel contains the statistic's name, while valueLabel contains the average value to date. We can set these values using the setTitle: setDecimalValue: and setCurrencyValue: methods. Now let's look at the definitions.

viewDidLoad method

- (void)viewDidLoad {
    [super viewDidLoad];
   
    titleLabel.text = @"undefined";
    valueLabel.text = @"undefined";
}

This method is called after the view has loaded. It simply sets the title and value labels to "undefined".

shouldAutorotateToInterfaceOrientation: and didReceiveMemoryWarning methods

- (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview // Release anything that's not essential, such as cached data }

These are UIView method stubs. We've seen them before. Again, the first one simply enables auto rotation. didRecieveMemoryWarning is simply the unmodified stub.

setTitle:, setDecimalValue: and setCurrencyValue:

- (void)setTitle:(NSString*)title {
   titleLabel.text = title;
}
- (void)setDecimalValue:(double)value {   
   valueLabel.text = [NSString decimal:value];
}
- (void)setCurrencyValue:(double)value {
   valueLabel.text = [NSString currency:value];
}

These methods set our label's text. Notice that the value setters use the NSString category we defined earlier. Be sure to import Formatter.h at the top of this file.

dealloc

- (void)dealloc {
    [model release];
    [titleLabel release];
    [valueLabel release];
   
    [super dealloc];
}

Finally, we release our model and labels. That's it for these classes; however, we won't use them directly. Instead, we will make a subclass for each individual view. Go ahead and make a MPGViewController. I recommend basing it off the NSObject template. We won't need any of the UIViewController stubs. Edit MPGViewController.h as shown below.

MPGViewController.h

#import <UIKit/UIKit.h>
#import "StatsViewController.h"
@interface MPGViewController : StatsViewController {
}
@end

The implementation is almost as simple. We set the title once, when the view first loads, but we update the value each time the view appears.

MPGViewController.m

#import "MPGViewController.h"
#import "Model.h"
@implementation MPGViewController
- (void)viewDidLoad {
    
    [super viewDidLoad];
    [self setTitle:@"Miles Per Gallon"];
}
- (void)viewWillAppear:(BOOL)animated {
    
    [self setDecimalValue:[self.model milesPerGallon]];
}
@end

That's it for the code, now we just need to wire everything together. Save these files, then open StatsView.xib. Add two labels to the view, as shown below.


I recommend changing the Title's font size before positioning it. Open the fonts window by selecting Font ... Show Fonts. I changed the size to 24 pts. Once that's done, resize the label by selecting Layout ... Size To Fit. Then place the Label at the top of the view, and stretch it until it fills the view from margin to margin. You then center the text from the Attributes Inspector.

Note: the font controls are somewhat scattered. Font family, typeface and size are controlled by the Fonts window. Alignment and color are in the Attributes Inspector.

Now, change the File's Owner's class to MPGViewController. Make sure the view, titleLabel and valueLabel outlets are connected properly, and save the nib file. That's half our connections.

Open MainWindow.xib. Single click the MPG tab, and make sure the Inspector is showing the View Controller Identity information. Change the class to MPGViewController. Next, right click the tab, and make sure the model outlet is connected to the model object.

That's it. Save everything, then launch the application and take it for a spin.

Conclusion

Now, we've covered a lot of ground in a very short time. Let's quickly review the main points:

We managed our view hierarchy by pushing and popping views from the navigation controller.

We validated and formatted the text in our text fields.

We created a delegate to both fill our table and manage row selections.

We added rows to and removed rows from our table, including animations.

We wrote a category to extend an existing class.

We wrote an extension to add private methods to a class.

We examined the finer points of memory management for IBOutlets.

Of course, there's still a lot of work to be done. There's no way to edit an entry, and there's no way to set a custom date. The application also desperately needs more testing. For example, the current model works fine for a dozen or so entries, but what happens when the user enters hundreds or thousands?

Still, I wanted to show you something that was more than just a toy project. I hope working on GasTracker has given you a more-complete view of the entire iPhone app development process.

So, that's it. Get out there and make something great.


Rich Warren lives in Honolulu, Hawaii with his wife, Mika, daughter, Haruko, and his son, Kai. He is a software engineer, freelance writer and part time graduate student. When not playing on the beach, he is probably writing, coding or doing research on his MacBook Pro. You can reach Rich at rikiwarren@mac.com, check out his blog at http://freelancemadscience.blogspot.com/ or follow him at http://twitter.com/rikiwarren.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Adobe After Effects CC 2018 16.1.3 - Cre...
After Effects CC 2018 is available as part of Adobe Creative Cloud for $52.99/month (or $20.99/month for a single app license). The new, more connected After Effects CC 2018 can make the impossible... Read more
Adobe Audition CC 2019 12.1.4 - Professi...
Audition CC 2019 is available as part of Adobe Creative Cloud for as little as $20.99/month (or $9.99/month if you're a previous Audition customer). Adobe Audition CC 2019 empowers you to create and... Read more
Adobe Premiere Pro CC 2019 13.1.5 - Digi...
Premiere Pro CC 2019 is available as part of Adobe Creative Cloud for as little as $52.99/month. The price on display is a price for annual by-monthly plan for Adobe Premiere Pro only Adobe Premiere... Read more
Navicat Premium Essentials 12.1.25 - Pro...
Navicat Premium Essentials is a compact version of Navicat which provides basic and necessary features you will need to perform simple administration on a database. It supports the latest features... Read more
Sketch 58 - Design app for UX/UI for iOS...
Sketch is an innovative and fresh look at vector drawing. Its intentionally minimalist design is based upon a drawing space of unlimited size and layers, free of palettes, panels, menus, windows, and... Read more
ClipGrab 3.8.5 - Download videos from Yo...
ClipGrab is a free downloader and converter for YouTube, Vimeo, Facebook and many other online video sites. It converts downloaded videos to MPEG4, MP3 or other formats in just one easy step Version... Read more
Dash 4.6.6 - Instant search and offline...
Dash is an API documentation browser and code snippet manager. Dash helps you store snippets of code, as well as instantly search and browse documentation for almost any API you might use (for a full... Read more
FotoMagico 5.6.8 - Powerful slideshow cr...
FotoMagico lets you create professional slideshows from your photos and music with just a few, simple mouse clicks. It sports a very clean and intuitive yet powerful user interface. High image... Read more
Civilization VI 1.2.4 - Next iteration o...
Sid Meier’s Civilization VI is the next entry in the popular Civilization franchise. Originally created by legendary game designer Sid Meier, Civilization is a strategy game in which you attempt to... Read more
Skype 8.52.0.138 - Voice-over-internet p...
Skype allows you to talk to friends, family and co-workers across the Internet without the inconvenience of long distance telephone charges. Using peer-to-peer data transmission technology, Skype... Read more

Latest Forum Discussions

See All

Lots of premium games are going free (so...
You may have seen over the past couple weeks a that a bunch of premium games have suddenly become free. This isn’t a mistake, nor is it some last hurrah before Apple Arcade hits, and it’s important to know that these games aren’t actually becoming... | Read more »
Yoozoo Games launches Saint Seiya Awaken...
If you’re into your anime, you’ve probably seen or heard of Saint Seiya. Based on a shonen manga by Masami Kurumada, the series was massively popular in the 1980s – especially in its native Japan. Since then, it’s grown into a franchise of all... | Read more »
Five Nights at Freddy's AR: Special...
Five Nights at Freddy's AR: Special Delivery is a terrifying new nightmare from developer Illumix. Last week, FNAF fans were sent into a frenzy by a short teaser for what we now know to be Special Delivery. Those in the comments were quick to... | Read more »
Rush Rally 3's new live events are...
Last week, Rush Rally 3 got updated with live events, and it’s one of the best things to happen to racing games on mobile. Prior to this update, the game already had multiplayer, but live events are more convenient in the sense that it’s somewhat... | Read more »
Why your free-to-play racer sucks
It’s been this way for a while now, but playing Hot Wheels Infinite Loop really highlights a big issue with free-to-play mobile racing games: They suck. It doesn’t matter if you’re trying going for realism, cart racing, or arcade nonsense, they’re... | Read more »
Steam Link Spotlight - The Banner Saga 3
Steam Link Spotlight is a new feature where we take a look at PC games that play exceptionally well using the Steam Link app. Our last entry talked about Terry Cavanaugh’s incredible Dicey Dungeons. Read about how it’s a great mobile experience... | Read more »
Combo Quest (Games)
Combo Quest 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: Combo Quest is an epic, time tap role-playing adventure. In this unique masterpiece, you are a knight on a heroic quest to retrieve... | Read more »
Hero Emblems (Games)
Hero Emblems 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: ** 25% OFF for a limited time to celebrate the release ** ** Note for iPhone 6 user: If it doesn't run fullscreen on your device... | Read more »
Puzzle Blitz (Games)
Puzzle Blitz 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: Puzzle Blitz is a frantic puzzle solving race against the clock! Solve as many puzzles as you can, before time runs out! You have... | Read more »
Sky Patrol (Games)
Sky Patrol 1.0.1 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0.1 (iTunes) Description: 'Strategic Twist On The Classic Shooter Genre' - Indie Game Mag... | Read more »

Price Scanner via MacPrices.net

$250 prepaid Visa card with any Apple iPhone,...
Xfinity Mobile will include a free $250 prepaid Visa card with the purchase of any new iPhone, new line activation, and transfer of phone number to Xfinity Mobile. Offer is valid through October 27,... Read more
Sprint is offering the 64GB Apple iPhone 11 P...
Sprint has the new 64GB iPhone 11 Pro available for $12.50 per month for new customers with an eligible trade-in in of iPhone 7 or newer. That’s down from their standard monthly lease of $41.67. The... Read more
Final week: Apple’s 2019 Back to School Promo...
Purchase a new Mac using Apple’s Education discount, and take up to $400 off MSRP. All teachers, students, and staff of any educational institution with a .edu email address qualify for the discount... Read more
Save $30 on Apple’s AirPods at these reseller...
Amazon is offering discounts on new 2019 Apple AirPods ranging up to $30 off MSRP as part of their Labor Day sale. Shipping is free: – AirPods with Charging Case: $144.95 $15 off MSRP – AirPods with... Read more
Preorder your Apple Watch Series 5 today at A...
Amazon has Apple Watch Series 5 GPS models available for preorder and on sale today for $15 off Apple’s MSRP. Shipping is free and starts on September 20th: – 40mm Apple Watch Series 5 GPS: $384.99 $... Read more
21″ iMacs on sale for $100 off Apple’s MSRP,...
B&H Photo has new 21″ Apple iMacs on sale for $100 off MSRP with models available starting at $999. These are the same iMacs offered by Apple in their retail and online stores. Overnight shipping... Read more
2018 4 and 6-Core Mac minis on sale today for...
Apple resellers are offering new 2018 4-Core and 6-Core Mac minis for $100-$150 off MSRP for a limited time. B&H Photo has the new 2018 4-Core and 6-Core Mac minis on sale for up to $150 off... Read more
Save $150-$250 on 10.2″ WiFi + Cellular iPads...
Verizon is offering $150-$250 discounts on Apple’s new 10.2″ WiFi + Cellular iPad with service. Buy the iPad itself and save $150. Save $250 on the purchase of an iPad along with an iPhone. The fine... Read more
Apple continues to offer 13″ 2.3GHz Dual-Core...
Apple has Certified Refurbished 2017 13″ 2.3GHz Dual-Core non-Touch Bar MacBook Pros available starting at $1019. An standard Apple one-year warranty is included with each model, outer cases are new... Read more
Apple restocks 2018 MacBook Airs, Certified R...
Apple has restocked Certified Refurbished 2018 13″ MacBook Airs starting at only $849. Each MacBook features a new outer case, comes with a standard Apple one-year warranty, and is shipped free. The... Read more

Jobs Board

Student Employment (Blue *Apple* Cafe) Spri...
Student Employment (Blue Apple Cafe) Spring 2019 Penn State University Campus/Location: Penn State Brandywine Campus City: Media, PA Date Announced: 12/20/2018 Date Read more
Geek Squad *Apple* Master Consultation Agen...
**732907BR** **Job Title:** Geek Squad Apple Master Consultation Agent **Job Category:** Services/Installation/Repair **Location Number:** 000360-Williston-Store Read more
*Apple* Mobile Master - Best Buy (United Sta...
**728519BR** **Job Title:** Apple Mobile Master **Job Category:** Store Associates **Location Number:** 000853-Jackson-Store **Job Description:** **What does a Best Read more
*Apple* Mobility Pro - Best Buy (United Stat...
**733006BR** **Job Title:** Apple Mobility Pro **Job Category:** Store Associates **Location Number:** 000865-Conroe-Store **Job Description:** At Best Buy, our Read more
*Apple* Mobility Pro-Store 149 - Best Buy (U...
**731985BR** **Job Title:** Apple Mobility Pro-Store 149 **Job Category:** Store Associates **Location Number:** 000149-Towson-Store **Job Description:** At Best Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.