How to Create a Recipes App for the iPhone

How to Create a Recipes App for the iPhone
Recipes project is a book of recipes. It’s interesting because it covers a large number of
classes and protocols of UIKit:










UITabBarController,
UIViewController,
UITableViewController,
UIView,
UIButton,
UITextField,
UILabel,
UIImageView,
UISegmentedControl,
UIPickerView and so on.
This sample demonstrates the various options of their using.
Screenshots of the app:
Figure 3
Figure 2
Figure 1
Main class is UITabBarController, which allows switching between
two UINavigationControllers.
The first one is responsible for view navigation between controllers:
 Preview
 Edit
 Create recipes.
The second UINavigationController (on the second tab) presents services of conversion (kg<>pounds and
1
Celsius <> Fahrenheit).
Create a new project based on Tab Bar Application template.
Incude CoreData.framework.
Create CoreData model: NewFile → CoreData → DataModel, name – Recipes.
Add following entities to it:
1. Recipe – with properties:





instructions : type String,
name : type String,
overview : type String,
prepTime : type String,
thumbnailImage : type Transformable;
2. RecipeType – with properties:
 name : type String;
3. Image – with properties:
 Image : type Transformable;
4. Ingredient – with properties:
 amount : type String,
 displayOrder : type int16,
 name : type String;
Figure 4
Add relationships as on fig. 4
2
Add next properties to RecipesAppDelegate:
NSManagedObjectModel *managedObjectModel;
NSManagedObjectContext *managedObjectContext;
NSPersistentStoreCoordinator *persistentStoreCoordinator;
//for database functioning;
RecipeListTableViewController *recipeListController;
//the first TableViewController
RecipesAppDelegate.h
#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
@class RecipeListTableViewController;
@interface MyRecipesAppDelegate : NSObject <UIApplicationDelegate> {
NSManagedObjectModel *managedObjectModel;
NSManagedObjectContext *managedObjectContext;
NSPersistentStoreCoordinator *persistentStoreCoordinator;
UIWindow *window;
UITabBarController *tabBarController;
RecipeListTableViewController *recipeListController;
}
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UITabBarController *tabBarController;
@property (nonatomic, retain) IBOutlet RecipeListTableViewController *recipeListController;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;
- (NSString *)applicationDocumentsDirectory;
@end
RecipesAppDelegate.m
#import "RecipesAppDelegate.h"
#import "RecipeListTableViewController.h"
#import "UnitConverterTableViewController.h"
@implementation RecipesAppDelegate
@synthesize window;
@synthesize tabBarController;
@synthesize recipeListController;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
recipeListController.managedObjectContext = self.managedObjectContext;
[window addSubview:tabBarController.view];
[window makeKeyAndVisible];
}
/**
applicationWillTerminate: saves changes in the application's managed object context before the application terminates.
*/
- (void)applicationWillTerminate:(UIApplication *)application {
NSError *error;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
3
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}
#pragma mark #pragma mark Core Data stack
/**
Returns the managed object context for the application.
If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
*/
- (NSManagedObjectContext *)managedObjectContext {
if (managedObjectContext != nil) {
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [NSManagedObjectContext new];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}
/**
Returns the managed object model for the application.
If the model doesn't already exist, it is created by merging all of the models found in the application bundle.
*/
- (NSManagedObjectModel *)managedObjectModel {
if (managedObjectModel != nil) {
return managedObjectModel;
}
managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];
return managedObjectModel;
}
/**
Returns the persistent store coordinator for the application.
If the coordinator doesn't already exist, it is created and the application's store added to it.
*/
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"Recipes.sqlite"];
/*
Set up the store.
For the sake of illustration, provide a pre-populated default store.
*/
NSFileManager *fileManager = [NSFileManager defaultManager];
// If the expected store doesn't exist, copy the default store.
if (![fileManager fileExistsAtPath:storePath]) {
NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:@"Recipes" ofType:@"sqlite"];
if (defaultStorePath) {
[fileManager copyItemAtPath:defaultStorePath toPath:storePath error:NULL];
}
}
NSURL *storeUrl = [NSURL fileURLWithPath:storePath];
4
NSError *error;
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil
error:&error]) {
/*
Replace this implementation with code to handle the error appropriately
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return persistentStoreCoordinator;
}
#pragma mark #pragma mark Application's documents directory
/**
Returns the path to the application's documents directory.
*/
- (NSString *)applicationDocumentsDirectory {
return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}
#pragma mark #pragma mark Memory management
- (void)dealloc {
[managedObjectContext release];
[managedObjectModel release];
[persistentStoreCoordinator release];
[recipeListController release];
[tabBarController release];
[window release];
[super dealloc];
}
@end
5
MainWindow.xib
In the Objects bar (usually located at the left side) do this:
remove FirstViewController and SecondViewController,
add 2 NavigationControllers instead of them.
Figure 5
6
Select one Navigation controller Item. On the Attribute Inspector tab:
set name of NavigationController = “Recipes”.
Do the same for “Unit Converter”.
And now we need to create root ViewControllers: RecipeListTableViewController and
UnitConverterTableViewController.
Create RecipeListTableViewController that inherits class UITableViewController.
Add following properties to it:
NSFetchedResultsController *fetchedResultsController;
NSManagedObjectContext *managedObjectContext;
RecipeListTableViewController should be supported some protocols:
 RecipeAddDelegate
 NSFetchedResultsControllerDelegate.
In this case, using of protocol RecipeAddDelegate doesn’t have much importance, it’s only for
learning. But using of delegate protocols is a good practice in large apps. They improve the readability
of code.
NSFetchedResultsController and NSFetchedResultsControllerDelegate are
library class and protocol for connection UITableView with CoreData. Object
NSFetchedResultsController contains data from database, and class implemented protocol
NSFetchedResultsControllerDelegate receives events if data has been changed.
Let look at method “viewDidLoad”:
self.navigationItem.leftBarButtonItem = self.editButtonItem;
/*This is a native category UIViewController(UIViewControllerEditing) that provides next
functionality: button (UIBarButtonItem *)editButtonItem and associated with this button
method “setEditing:bool animated:bool”.
User press on the edit button to switch edit/normal mode. By default edit mode has style –
UITableViewCellEditingStyleDelete for deleting rows. When we’ll create class
RecipeDetailViewController we pay more attention to this issue. We assign functions of
edit button to left navigation bar button.*/
UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add:)];
self.navigationItem.rightBarButtonItem = addButtonItem;
[addButtonItem release];
/*set new addButton (System style) as right navigation bar button with target action:
(void)add:(id)sender.
Pressing this button leads to pushing RecipeAddViewController, which allows edit name of
new recipe. */
In the method “tableView: cellForRowAtIndexPath:” at the row
recipeCell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
we add disclosure button to cell (like “>”).
7
Method “tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath”
is called when cell of tableView is selected in edit mode. In this case, we have only delete
operation for removing cell and its data source row.
Method :
fetchedResultsController
return initialized object NSFetchedResultsController.
Comment out methods:
 add:
 tableView: didSelectRowAtIndexPath: .
RecipeListTableViewController.h
#import "RecipeAddViewController.h"
@class Recipe;
@class RecipeTableViewCell;
@interface RecipeListTableViewController : UITableViewController <RecipeAddDelegate, NSFetchedResultsControllerDelegate> {
@private
NSFetchedResultsController *fetchedResultsController;
NSManagedObjectContext *managedObjectContext;
}
@property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
- (void)showRecipe:(Recipe *)recipe animated:(BOOL)animated;
- (void)configureCell:(RecipeTableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;
@end
RecipeListTableViewController.m
#import "RecipeListTableViewController.h"
#import "RecipeDetailViewController.h"
#import "Recipe.h"
#import "RecipeTableViewCell.h"
@implementation RecipeListTableViewController
@synthesize managedObjectContext, fetchedResultsController;
#pragma mark #pragma mark UIViewController overrides
- (void)viewDidLoad {
// Configure the navigation bar
self.title = @"Recipes";
self.navigationItem.leftBarButtonItem = self.editButtonItem;
UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
target:self action:@selector(add:)];
self.navigationItem.rightBarButtonItem = addButtonItem;
[addButtonItem release];
// Set the table view's row height
self.tableView.rowHeight = 44.0;
NSError *error = nil;
8
if (![[self fetchedResultsController] performFetch:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping
application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that
instructs the user to quit the application by pressing the Home button.
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Support all orientations except upside down
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
#pragma mark #pragma mark Recipe support
- (void)add:(id)sender {
// To add a new recipe, create a RecipeAddViewController. Present it as a modal view so that the user's focus is on the task of
adding the recipe; wrap the controller in a navigation controller to provide a navigation bar for the Done and Save buttons (added by
the RecipeAddViewController in its viewDidLoad method).
RecipeAddViewController *addController = [[RecipeAddViewController alloc] initWithNibName:@"RecipeAddView"
bundle:nil];
addController.delegate = self;
Recipe *newRecipe = [NSEntityDescription insertNewObjectForEntityForName:@"Recipe"
inManagedObjectContext:self.managedObjectContext];
addController.recipe = newRecipe;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController];
[self presentModalViewController:navigationController animated:YES];
[navigationController release];
[addController release];
// [self.tableView setEditing:YES animated:YES];
// self.tableView.allowsSelectionDuringEditing = YES;
}
- (void)recipeAddViewController:(RecipeAddViewController *)recipeAddViewController didAddRecipe:(Recipe *)recipe {
if (recipe) {
// Show the recipe in a new view controller
[self showRecipe:recipe animated:NO];
}
// Dismiss the modal add recipe view controller
[self dismissModalViewControllerAnimated:YES];
}
- (void)showRecipe:(Recipe *)recipe animated:(BOOL)animated {
// Create a detail view controller, set the recipe, then push it.
RecipeDetailViewController *detailViewController = [[RecipeDetailViewController alloc]
initWithStyle:UITableViewStyleGrouped];
detailViewController.recipe = recipe;
[self.navigationController pushViewController:detailViewController animated:animated];
[detailViewController release];
}
#pragma mark #pragma mark Table view methods
9
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSInteger count = [[fetchedResultsController sections] count];
if (count == 0) {
count = 1;
}
return count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger numberOfRows = 0;
if ([[fetchedResultsController sections] count] > 0) {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
numberOfRows = [sectionInfo numberOfObjects];
}
return numberOfRows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Dequeue or if necessary create a RecipeTableViewCell, then set its recipe to the recipe for the current row.
static NSString *RecipeCellIdentifier = @"RecipeCellIdentifier";
RecipeTableViewCell *recipeCell = (RecipeTableViewCell *)[tableView
dequeueReusableCellWithIdentifier:RecipeCellIdentifier];
if (recipeCell == nil) {
recipeCell = [[[RecipeTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:RecipeCellIdentifier]
autorelease];
recipeCell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
[self configureCell:recipeCell atIndexPath:indexPath];
return recipeCell;
}
- (void)configureCell:(RecipeTableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
// Configure the cell
Recipe *recipe = (Recipe *)[fetchedResultsController objectAtIndexPath:indexPath];
cell.recipe = recipe;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
Recipe *recipe = (Recipe *)[fetchedResultsController objectAtIndexPath:indexPath];
[self showRecipe:recipe animated:YES];
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object for the given index path
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
[context deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];
// Save the context.
NSError *error;
if (![context save:&error]) {
/*
Replace this implementation with code to handle the error appropriately
*/
10
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}
/*
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCellEditingStyle style = UITableViewCellEditingStyleNone;
return style;
}*/
#pragma mark #pragma mark Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController {
// Set up the fetched results controller if needed.
if (fetchedResultsController == nil) {
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Recipe"
inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:@"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
}
return fetchedResultsController;
}
/**
Delegate methods of NSFetchedResultsController to respond to additions, removals and so on.
*/
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
11
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:(RecipeTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
#pragma mark #pragma mark Memory management
- (void)dealloc {
[fetchedResultsController release];
[managedObjectContext release];
[super dealloc];
}
@end
Create class for tableView cell – RecipeTableViewCell. Copy code.
RecipeTableViewCell.h
#import "Recipe.h"
@interface RecipeTableViewCell : UITableViewCell {
Recipe *recipe;
UIImageView *imageView;
UILabel *nameLabel;
UILabel *overviewLabel;
UILabel *prepTimeLabel;
}
@property (nonatomic, retain) Recipe *recipe;
12
@property (nonatomic, retain) UIImageView *imageView;
@property (nonatomic, retain) UILabel *nameLabel;
@property (nonatomic, retain) UILabel *overviewLabel;
@property (nonatomic, retain) UILabel *prepTimeLabel;
@end
RecipeTableViewCell.m
#import "RecipeTableViewCell.h"
#pragma mark #pragma mark SubviewFrames category
@interface RecipeTableViewCell (SubviewFrames)
- (CGRect)_imageViewFrame;
- (CGRect)_nameLabelFrame;
- (CGRect)_descriptionLabelFrame;
- (CGRect)_prepTimeLabelFrame;
@end
#pragma mark #pragma mark RecipeTableViewCell implementation
@implementation RecipeTableViewCell
@synthesize recipe, imageView, nameLabel, overviewLabel, prepTimeLabel;
#pragma mark #pragma mark Initialization
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
imageView = [[UIImageView alloc] initWithFrame:CGRectZero];
imageView.contentMode = UIViewContentModeScaleAspectFit;
[self.contentView addSubview:imageView];
overviewLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[overviewLabel setFont:[UIFont systemFontOfSize:12.0]];
[overviewLabel setTextColor:[UIColor darkGrayColor]];
[overviewLabel setHighlightedTextColor:[UIColor whiteColor]];
[self.contentView addSubview:overviewLabel];
prepTimeLabel = [[UILabel alloc] initWithFrame:CGRectZero];
prepTimeLabel.textAlignment = UITextAlignmentRight;
[prepTimeLabel setFont:[UIFont systemFontOfSize:12.0]];
[prepTimeLabel setTextColor:[UIColor blackColor]];
[prepTimeLabel setHighlightedTextColor:[UIColor whiteColor]];
prepTimeLabel.minimumFontSize = 7.0;
prepTimeLabel.lineBreakMode = UILineBreakModeTailTruncation;
[self.contentView addSubview:prepTimeLabel];
nameLabel = [[UILabel alloc] initWithFrame:CGRectZero];
[nameLabel setFont:[UIFont boldSystemFontOfSize:14.0]];
[nameLabel setTextColor:[UIColor blackColor]];
[nameLabel setHighlightedTextColor:[UIColor whiteColor]];
[self.contentView addSubview:nameLabel];
}
return self;
}
#pragma mark #pragma mark Laying out subviews
13
/*
To save space, the prep time label disappears during editing.
*/
- (void)layoutSubviews {
[super layoutSubviews];
[imageView setFrame:[self _imageViewFrame]];
[nameLabel setFrame:[self _nameLabelFrame]];
[overviewLabel setFrame:[self _descriptionLabelFrame]];
[prepTimeLabel setFrame:[self _prepTimeLabelFrame]];
if (self.editing) {
prepTimeLabel.alpha = 0.0;
} else {
prepTimeLabel.alpha = 1.0;
}
}
#define IMAGE_SIZE
42.0
#define EDITING_INSET
10.0
#define TEXT_LEFT_MARGIN 8.0
#define TEXT_RIGHT_MARGIN 5.0
#define PREP_TIME_WIDTH 80.0
/*
Return the frame of the various subviews -- these are dependent on the editing state of the cell.
*/
- (CGRect)_imageViewFrame {
if (self.editing) {
return CGRectMake(EDITING_INSET, 0.0, IMAGE_SIZE, IMAGE_SIZE);
}
else {
return CGRectMake(0.0, 0.0, IMAGE_SIZE, IMAGE_SIZE);
}
}
- (CGRect)_nameLabelFrame {
if (self.editing) {
return CGRectMake(IMAGE_SIZE + EDITING_INSET + TEXT_LEFT_MARGIN, 4.0, self.contentView.bounds.size.width IMAGE_SIZE - EDITING_INSET - TEXT_LEFT_MARGIN, 16.0);
}
else {
return CGRectMake(IMAGE_SIZE + TEXT_LEFT_MARGIN, 4.0, self.contentView.bounds.size.width - IMAGE_SIZE TEXT_RIGHT_MARGIN * 2 - PREP_TIME_WIDTH, 16.0);
}
}
- (CGRect)_descriptionLabelFrame {
if (self.editing) {
return CGRectMake(IMAGE_SIZE + EDITING_INSET + TEXT_LEFT_MARGIN, 22.0, self.contentView.bounds.size.width IMAGE_SIZE - EDITING_INSET - TEXT_LEFT_MARGIN, 16.0);
}
else {
return CGRectMake(IMAGE_SIZE + TEXT_LEFT_MARGIN, 22.0, self.contentView.bounds.size.width - IMAGE_SIZE TEXT_LEFT_MARGIN, 16.0);
}
}
- (CGRect)_prepTimeLabelFrame {
CGRect contentViewBounds = self.contentView.bounds;
return CGRectMake(contentViewBounds.size.width - PREP_TIME_WIDTH - TEXT_RIGHT_MARGIN, 4.0,
PREP_TIME_WIDTH, 16.0);
}
#pragma mark #pragma mark Recipe set accessor
- (void)setRecipe:(Recipe *)newRecipe {
if (newRecipe != recipe) {
14
[recipe release];
recipe = [newRecipe retain];
}
imageView.image = recipe.thumbnailImage;
nameLabel.text = recipe.name;
overviewLabel.text = recipe.overview;
prepTimeLabel.text = recipe.prepTime;
}
#pragma mark #pragma mark Memory management
- (void)dealloc {
[recipe release];
[imageView release];
[nameLabel release];
[overviewLabel release];
[prepTimeLabel release];
[super dealloc];
}
@end
Create class RecipeAddViewController and add protocol declaration RecipeAddDelegate
to .h file.
RecipeAddViewController.h
@protocol RecipeAddDelegate;
@class Recipe
@interface RecipeAddViewController : UIViewController <UITextFieldDelegate> {
@private
}
@end
@protocol RecipeAddDelegate <NSObject>
// recipe == nil on cancel
- (void)recipeAddViewController:(RecipeAddViewController *)recipeAddViewController didAddRecipe:(Recipe *)recipe;
@end
RecipeAddViewController.m
#import "RecipeAddViewController.h"
#import "Recipe.h"
@implementation RecipeAddViewController
@end
Create class UnitConverterTableViewController, comment out method
 tableView: didSelectRowAtIndexPath: .
UnitConverterTableViewController.h
@interface UnitConverterTableViewController : UITableViewController {
}
@end
UnitConverterTableViewController.m
#import "UnitConverterTableViewController.h"
#import "WeightConverterViewController.h"
#import "TemperatureConverterViewController.h"
15
@implementation UnitConverterTableViewController
- (id)init {
return [self initWithStyle:UITableViewStyleGrouped];
}
- (id)initWithStyle:(UITableViewStyle)style {
if (self = [super initWithStyle:style]) {
self.title = @"Unit Converter";
}
return self;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
#define WeightConverterIndex 0
#define TemperatureConverterIndex 1
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease];
}
switch ([indexPath section]) {
case WeightConverterIndex:
cell.textLabel.text = @"Weight";
break;
case TemperatureConverterIndex:
cell.textLabel.text = @"Temperature";
break;
}
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIViewController *converterController = nil;
switch ([indexPath section]) {
case WeightConverterIndex:
converterController = [[WeightConverterViewController alloc] initWithNibName:@"WeightConverter" bundle:nil];
break;
case TemperatureConverterIndex:
converterController = [[TemperatureConverterViewController alloc] initWithNibName:@"TemperatureConverter"
bundle:nil];
break;
}
if (converterController) {
[self.navigationController pushViewController:converterController animated:YES];
[converterController release];
}
}
16
@end
17
Let look at MainWindow.xib again.
In the Identity Inspector set class RecipeListTableViewController for UIViewController in the tree of
“Navigation Controller – Recipes” and set similarly class UnitConverterTableViewController for
UIViewController in the tree of “Navigation Controller - Unit Conversion”.
Figure 6
18
In the Objects bar click Recipes App Delegate and on the Connections Inspector tab link property
recipeListController to RecipeListTableViewController in the Objects bar.
Figure 7
Figure 8
Set name of Navigation Item in the tree of UnitConverterTableViewController = “Unit Conversion” in the
Attribute Inspector tab:
If we run app now we’ll see empty table of recipes on the first tab and Unit Conversation view with disabled
cells on the second tab.
Now we have a working skeleton application!
19
RecipeAddViewController.
It allows to set name of new recipe. It has a UITextField object.
Add properties:
Recipe *recipe;
UITextField *nameTextField;
id <RecipeAddDelegate> delegate;
Declare this class implements UITextFieldDelegate protocol and add method – “textFieldShouldReturn:” of
this protocol. In the method viewDidLoad we create two buttons with target actions.
In RecipeAddViewController.xib:
 Drag and drop a new TextField object to View;
 In the Attribute Inspector tab for the TextField object set property Placeholder = “Recipe Name”;
 Font size = 17;
 In the Connection Inspector tab for this TextField link delegate to File's Owner.
RecipeAddViewController.h
@protocol RecipeAddDelegate;
@class Recipe;
@interface RecipeAddViewController : UIViewController <UITextFieldDelegate> {
@private
Recipe *recipe;
UITextField *nameTextField;
id <RecipeAddDelegate> delegate;
}
@property(nonatomic, retain) Recipe *recipe;
@property(nonatomic, retain) IBOutlet UITextField *nameTextField;
@property(nonatomic, assign) id <RecipeAddDelegate> delegate;
- (void)save;
- (void)cancel;
@end
@protocol RecipeAddDelegate <NSObject>
// recipe == nil on cancel
- (void)recipeAddViewController:(RecipeAddViewController *)recipeAddViewController didAddRecipe:(Recipe *)recipe;
@end
RecipeAddViewController.m
#import "RecipeAddViewController.h"
#import "Recipe.h"
@implementation RecipeAddViewController
@synthesize recipe;
@synthesize nameTextField;
@synthesize delegate;
- (void)viewDidLoad {
// Configure the navigation bar
self.navigationItem.title = @"Add Recipe";
UIBarButtonItem *cancelButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStyleBordered
target:self action:@selector(cancel)];
self.navigationItem.leftBarButtonItem = cancelButtonItem;
[cancelButtonItem release];
20
UIBarButtonItem *saveButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleDone
target:self action:@selector(save)];
self.navigationItem.rightBarButtonItem = saveButtonItem;
[saveButtonItem release];
[nameTextField becomeFirstResponder];
}
- (void)viewDidUnload {
self.nameTextField = nil;
[super viewDidUnload];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Support all orientations except upside-down
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
if (textField == nameTextField) {
[nameTextField resignFirstResponder];
[self save];
}
return YES;
}
- (void)save {
recipe.name = nameTextField.text;
NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
[self.delegate recipeAddViewController:self didAddRecipe:recipe];
}
- (void)cancel {
[recipe.managedObjectContext deleteObject:recipe];
NSError *error = nil;
if (![recipe.managedObjectContext save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
[self.delegate recipeAddViewController:self didAddRecipe:nil];
}
- (void)dealloc {
[recipe release];
[nameTextField release];
[super dealloc];
}
21
@end
22
RecipeDetailViewController.
Create class RecipeDetailViewController inherits UITableViewController class and implements protocols:
UINavigationControllerDelegate, UIImagePickerControllerDelegate, UITextFieldDelegate.
Add properties:
Recipe *recipe;
NSMutableArray *ingredients;
UIView *tableHeaderView;
UIButton *photoButton;
UITextField *nameTextField;
UITextField *overviewTextField;
UITextField *prepTimeTextField;
RecipeDetailViewController.h
@class Recipe;
@interface RecipeDetailViewController : UITableViewController <UINavigationControllerDelegate,
UIImagePickerControllerDelegate, UITextFieldDelegate> {
@private
Recipe *recipe;
NSMutableArray *ingredients;
UIView *tableHeaderView;
UIButton *photoButton;
UITextField *nameTextField;
UITextField *overviewTextField;
UITextField *prepTimeTextField;
}
@property (nonatomic, retain) Recipe *recipe;
@property (nonatomic, retain) NSMutableArray *ingredients;
@property (nonatomic, retain) IBOutlet UIView *tableHeaderView;
@property (nonatomic, retain) IBOutlet UIButton *photoButton;
@property (nonatomic, retain) IBOutlet UITextField *nameTextField;
@property (nonatomic, retain) IBOutlet UITextField *overviewTextField;
@property (nonatomic, retain) IBOutlet UITextField *prepTimeTextField;
- (IBAction)photoTapped;
@end
RecipeDetailViewController.m
#import "RecipeDetailViewController.h"
#import "Recipe.h"
#import "Ingredient.h"
#import "InstructionsViewController.h"
#import "TypeSelectionViewController.h"
#import "RecipePhotoViewController.h"
#import "IngredientDetailViewController.h"
@interface RecipeDetailViewController (PrivateMethods)
- (void)updatePhotoButton;
@end
@implementation RecipeDetailViewController
23
@synthesize recipe;
@synthesize ingredients;
@synthesize tableHeaderView;
@synthesize photoButton;
@synthesize nameTextField, overviewTextField, prepTimeTextField;
#define TYPE_SECTION 0
#define INGREDIENTS_SECTION 1
#define INSTRUCTIONS_SECTION 2
#pragma mark #pragma mark View controller
- (void)viewDidLoad {
self.navigationItem.rightBarButtonItem = self.editButtonItem;
// Create and set the table header view.
if (tableHeaderView == nil) {
[[NSBundle mainBundle] loadNibNamed:@"DetailHeaderView" owner:self options:nil];
self.tableView.tableHeaderView = tableHeaderView;
self.tableView.allowsSelectionDuringEditing = YES;
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[photoButton setImage:recipe.thumbnailImage forState:UIControlStateNormal];
self.navigationItem.title = recipe.name;
nameTextField.text = recipe.name;
overviewTextField.text = recipe.overview;
prepTimeTextField.text = recipe.prepTime;
[self updatePhotoButton];
/*
Create a mutable array that contains the recipe's ingredients ordered by displayOrder.
The table view uses this array to display the ingredients.
Core Data relationships are represented by sets, so have no inherent order. Order is "imposed" using the displayOrder
attribute, but it would be inefficient to create and sort a new array each time the ingredients section had to be laid out or updated.
*/
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"displayOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:&sortDescriptor count:1];
NSMutableArray *sortedIngredients = [[NSMutableArray alloc] initWithArray:[recipe.ingredients allObjects]];
[sortedIngredients sortUsingDescriptors:sortDescriptors];
self.ingredients = sortedIngredients;
[sortDescriptor release];
[sortDescriptors release];
[sortedIngredients release];
// Update recipe type and ingredients on return.
[self.tableView reloadData];
}
- (void)viewDidUnload {
self.tableHeaderView = nil;
self.photoButton = nil;
self.nameTextField = nil;
self.overviewTextField = nil;
self.prepTimeTextField = nil;
[super viewDidUnload];
}
24
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
#pragma mark #pragma mark Editing
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[self updatePhotoButton];
nameTextField.enabled = editing;
overviewTextField.enabled = editing;
prepTimeTextField.enabled = editing;
[self.navigationItem setHidesBackButton:editing animated:YES];
[self.tableView beginUpdates];
NSUInteger ingredientsCount = [recipe.ingredients count];
NSArray *ingredientsInsertIndexPath = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:ingredientsCount
inSection:INGREDIENTS_SECTION]];
if (editing) {
[self.tableView insertRowsAtIndexPaths:ingredientsInsertIndexPath withRowAnimation:UITableViewRowAnimationTop];
overviewTextField.placeholder = @"Overview";
} else {
[self.tableView deleteRowsAtIndexPaths:ingredientsInsertIndexPath withRowAnimation:UITableViewRowAnimationTop];
overviewTextField.placeholder = @"";
}
[self.tableView endUpdates];
/*
If editing is finished, save the managed object context.
*/
if (!editing) {
NSManagedObjectContext *context = recipe.managedObjectContext;
NSError *error = nil;
if (![context save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
if (textField == nameTextField) {
recipe.name = nameTextField.text;
self.navigationItem.title = recipe.name;
}
else if (textField == overviewTextField) {
recipe.overview = overviewTextField.text;
}
else if (textField == prepTimeTextField) {
recipe.prepTime = prepTimeTextField.text;
}
return YES;
}
25
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return YES;
}
#pragma mark #pragma mark UITableView Delegate/Datasource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 4;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
NSString *title = nil;
// Return a title or nil as appropriate for the section.
switch (section) {
case TYPE_SECTION:
title = @"Category";
break;
case INGREDIENTS_SECTION:
title = @"Ingredients";
break;
default:
break;
}
return title;;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger rows = 0;
/*
The number of rows depends on the section.
In the case of ingredients, if editing, add a row in editing mode to present an "Add Ingredient" cell.
*/
switch (section) {
case TYPE_SECTION:
case INSTRUCTIONS_SECTION:
rows = 1;
break;
case INGREDIENTS_SECTION:
rows = [recipe.ingredients count];
if (self.editing) {
rows++;
}
break;
default:
break;
}
return rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = nil;
// For the Ingredients section, if necessary create a new cell and configure it with an additional label for the amount. Give the cell
a different identifier from that used for cells in other sections so that it can be dequeued separately.
if (indexPath.section == INGREDIENTS_SECTION) {
NSUInteger ingredientCount = [recipe.ingredients count];
NSInteger row = indexPath.row;
if (indexPath.row < ingredientCount) {
// If the row is within the range of the number of ingredients for the current recipe, then configure the cell to show the
ingredient name and amount.
static NSString *IngredientsCellIdentifier = @"IngredientsCell";
26
cell = [tableView dequeueReusableCellWithIdentifier:IngredientsCellIdentifier];
if (cell == nil) {
// Create a cell to display an ingredient.
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:IngredientsCellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryNone;
}
Ingredient *ingredient = [ingredients objectAtIndex:row];
cell.textLabel.text = ingredient.name;
cell.detailTextLabel.text = ingredient.amount;
} else {
// If the row is outside the range, it's the row that was added to allow insertion (see tableView:numberOfRowsInSection:) so
give it an appropriate label.
static NSString *AddIngredientCellIdentifier = @"AddIngredientCell";
cell = [tableView dequeueReusableCellWithIdentifier:AddIngredientCellIdentifier];
if (cell == nil) {
// Create a cell to display "Add Ingredient".
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:AddIngredientCellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
cell.textLabel.text = @"Add Ingredient";
}
} else {
// If necessary create a new cell and configure it appropriately for the section. Give the cell a different identifier from that used
for cells in the Ingredients section so that it can be dequeued separately.
static NSString *MyIdentifier = @"GenericCell";
cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
NSString *text = nil;
switch (indexPath.section) {
case TYPE_SECTION: // type -- should be selectable -> checkbox
text = [recipe.type valueForKey:@"name"];
cell.accessoryType = UITableViewCellAccessoryNone;
cell.editingAccessoryType = UITableViewCellAccessoryDisclosureIndicator;
break;
case INSTRUCTIONS_SECTION: // instructions
text = @"Instructions";
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.editingAccessoryType = UITableViewCellAccessoryNone;
break;
default:
break;
}
cell.textLabel.text = text;
}
return cell;
}
#pragma mark #pragma mark Editing rows
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSIndexPath *rowToSelect = indexPath;
NSInteger section = indexPath.section;
BOOL isEditing = self.editing;
// If editing, don't allow instructions to be selected
// Not editing: Only allow instructions to be selected
27
if ((isEditing && section == INSTRUCTIONS_SECTION) || (!isEditing && section != INSTRUCTIONS_SECTION)) {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
rowToSelect = nil;
}
return rowToSelect;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger section = indexPath.section;
UIViewController *nextViewController = nil;
/*
What to do on selection depends on what section the row is in.
For Type, Instructions, and Ingredients, create and push a new view controller of the type appropriate for the next screen.
*/
switch (section) {
case TYPE_SECTION:
nextViewController = [[TypeSelectionViewController alloc] initWithStyle:UITableViewStyleGrouped];
((TypeSelectionViewController *)nextViewController).recipe = recipe;
break;
case INSTRUCTIONS_SECTION:
nextViewController = [[InstructionsViewController alloc] initWithNibName:@"InstructionsView" bundle:nil];
((InstructionsViewController *)nextViewController).recipe = recipe;
break;
case INGREDIENTS_SECTION:
nextViewController = [[IngredientDetailViewController alloc] initWithStyle:UITableViewStyleGrouped];
((IngredientDetailViewController *)nextViewController).recipe = recipe;
if (indexPath.row < [recipe.ingredients count]) {
Ingredient *ingredient = [ingredients objectAtIndex:indexPath.row];
((IngredientDetailViewController *)nextViewController).ingredient = ingredient;
}
break;
default:
break;
}
// If we got a new view controller, push it .
if (nextViewController) {
[self.navigationController pushViewController:nextViewController animated:YES];
[nextViewController release];
}
}
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCellEditingStyle style = UITableViewCellEditingStyleNone;
// Only allow editing in the ingredients section.
// In the ingredients section, the last row (row number equal to the count of ingredients) is added automatically (see
tableView:cellForRowAtIndexPath:) to provide an insertion cell, so configure that cell for insertion; the other cells are configured for
deletion.
if (indexPath.section == INGREDIENTS_SECTION) {
// If this is the last item, it's the insertion row.
if (indexPath.row == [recipe.ingredients count]) {
style = UITableViewCellEditingStyleInsert;
}
else {
style = UITableViewCellEditingStyleDelete;
}
}
return style;
}
28
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
// Only allow deletion, and only in the ingredients section
if ((editingStyle == UITableViewCellEditingStyleDelete) && (indexPath.section == INGREDIENTS_SECTION)) {
// Remove the corresponding ingredient object from the recipe's ingredient list and delete the appropriate table view cell.
Ingredient *ingredient = [ingredients objectAtIndex:indexPath.row];
[recipe removeIngredientsObject:ingredient];
[ingredients removeObject:ingredient];
NSManagedObjectContext *context = ingredient.managedObjectContext;
[context deleteObject:ingredient];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationTop];
}
}
#pragma mark #pragma mark Moving rows
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
BOOL canMove = NO;
// Moves are only allowed within the ingredients section. Within the ingredients section, the last row (Add Ingredient) cannot be
moved.
if (indexPath.section == INGREDIENTS_SECTION) {
canMove = indexPath.row != [recipe.ingredients count];
}
return canMove;
}
- (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath
*)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath {
NSIndexPath *target = proposedDestinationIndexPath;
/*
Moves are only allowed within the ingredients section, so make sure the destination is in the ingredients section.
If the destination is in the ingredients section, make sure that it's not the Add Ingredient row -- if it is, retarget for the penultimate
row.
*/
NSUInteger proposedSection = proposedDestinationIndexPath.section;
if (proposedSection < INGREDIENTS_SECTION) {
target = [NSIndexPath indexPathForRow:0 inSection:INGREDIENTS_SECTION];
} else if (proposedSection > INGREDIENTS_SECTION) {
target = [NSIndexPath indexPathForRow:([recipe.ingredients count] - 1) inSection:INGREDIENTS_SECTION];
} else {
NSUInteger ingredientsCount_1 = [recipe.ingredients count] - 1;
if (proposedDestinationIndexPath.row > ingredientsCount_1) {
target = [NSIndexPath indexPathForRow:ingredientsCount_1 inSection:INGREDIENTS_SECTION];
}
}
return target;
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath
*)toIndexPath {
/*
Update the ingredients array in response to the move.
Update the display order indexes within the range of the move.
*/
Ingredient *ingredient = [ingredients objectAtIndex:fromIndexPath.row];
29
[ingredients removeObjectAtIndex:fromIndexPath.row];
[ingredients insertObject:ingredient atIndex:toIndexPath.row];
NSInteger start = fromIndexPath.row;
if (toIndexPath.row < start) {
start = toIndexPath.row;
}
NSInteger end = toIndexPath.row;
if (fromIndexPath.row > end) {
end = fromIndexPath.row;
}
for (NSInteger i = start; i <= end; i++) {
ingredient = [ingredients objectAtIndex:i];
ingredient.displayOrder = [NSNumber numberWithInteger:i];
}
}
#pragma mark #pragma mark Photo
- (IBAction)photoTapped {
// If in editing state, then display an image picker; if not, create and push a photo view controller.
if (self.editing) {
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.delegate = self;
[self presentModalViewController:imagePicker animated:YES];
[imagePicker release];
} else {
RecipePhotoViewController *recipePhotoViewController = [[RecipePhotoViewController alloc] init];
recipePhotoViewController.hidesBottomBarWhenPushed = YES;
recipePhotoViewController.recipe = recipe;
[self.navigationController pushViewController:recipePhotoViewController animated:YES];
[recipePhotoViewController release];
}
}
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)selectedImage
editingInfo:(NSDictionary *)editingInfo {
// Delete any existing image.
NSManagedObject *oldImage = recipe.image;
if (oldImage != nil) {
[recipe.managedObjectContext deleteObject:oldImage];
}
// Create an image object for the new image.
NSManagedObject *image = [NSEntityDescription insertNewObjectForEntityForName:@"Image"
inManagedObjectContext:recipe.managedObjectContext];
recipe.image = image;
// Set the image for the image managed object.
[image setValue:selectedImage forKey:@"image"];
// Create a thumbnail version of the image for the recipe object.
CGSize size = selectedImage.size;
CGFloat ratio = 0;
if (size.width > size.height) {
ratio = 44.0 / size.width;
} else {
ratio = 44.0 / size.height;
}
CGRect rect = CGRectMake(0.0, 0.0, ratio * size.width, ratio * size.height);
UIGraphicsBeginImageContext(rect.size);
[selectedImage drawInRect:rect];
recipe.thumbnailImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
30
[self dismissModalViewControllerAnimated:YES];
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
[self dismissModalViewControllerAnimated:YES];
}
- (void)updatePhotoButton {
/*
How to present the photo button depends on the editing state and whether the recipe has a thumbnail image.
* If the recipe has a thumbnail, set the button's highlighted state to the same as the editing state (it's highlighted if editing).
* If the recipe doesn't have a thumbnail, then: if editing, enable the button and show an image that says "Choose Photo" or
similar; if not editing then disable the button and show nothing.
*/
BOOL editing = self.editing;
if (recipe.thumbnailImage != nil) {
photoButton.highlighted = editing;
} else {
photoButton.enabled = editing;
if (editing) {
[photoButton setImage:[UIImage imageNamed:@"choosePhoto.png"] forState:UIControlStateNormal];
} else {
[photoButton setImage:nil forState:UIControlStateNormal];
}
}
}
#pragma mark #pragma mark dealloc
- (void)dealloc {
[tableHeaderView release];
[photoButton release];
[nameTextField release];
[overviewTextField release];
[prepTimeTextField release];
[recipe release];
[ingredients release];
[super dealloc];
}
@end
Create file DetailHeaderView.xib.
Drag and drop new elements on View:
 UIButton
 UITextField Recipe Name (property Placeholder = “Recipe name”)
 UITextField Description (поле Placeholder = “Descrtiption”)
 UITextField Time (поле Placeholder = “Time”)
 UILabel (property Text = “Preparation time:”).
For UIButton in the Attribute Inspector tab set:
 type = Custom
 image = choosePhoto.png
For UITextField Recipe Name:
 font = 17, Border Stile = left button
For UITextField Description and Time:
 font = 14, Border Stile = left button
Click File's Owner in the Placeholders bar and set its class to RecipeDetailViewController.
On the Connection Inspector tab link properties to objects of the Objects bar:
31





tableHeaderView – View
prepTimeTextField - Text Field Time
overviewTextField - Text Field Description
nameTextField - Text Field Recipe Name
photoButton – Button
RecipeDetailViewController declares method “(IBAction)photoTapped”. IBAction means that method is
called by event, generated GUI objects declared as IBOutlet. Assign photoTapped to Touch Up Inside button
event handler of photoButton.
 click Button in the Objects bar, link event Touch Up Inside on the Connection Inspector tab to File's
Owner:
Figure 9

and click in popup photoTapped:
Look at implemention of method photoTapped. Depending on the edit mode is on/off we launch
RecipePhotoViewController or UIImagePickerController.
UIImagePickerController allows choose saved media file from Album and get access to it in our app.
32
Figure 10
Create UIImagePickerController object, set his delegate and show it as modal view
controller.
For UITextViews link thier delegate to File's Owner.
Look at implemetion of method viewDidLoad. It creates button self.editButtonItem like
RecipeListTableViewController. But in this case, we don’t need default style (EditingStyleDelete). We
override methods:
 (UITableViewCellEditingStyle)tableView:(UITableView *)tableView
editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath,
 (void)setEditing:(BOOL)editing animated:(BOOL)animated
In the first method every cell gets own editing style.
The second method includes several operations:
 switch on/off editing mode;
 update states of navigation bar buttons;
 if it’s necessary insert/remove cells;
 refresh database.
We need to override method:
 (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath
*)indexPath
that is called by touching + or - (insert, delete). Implement nessesary operations (insert, delete and so on).
After that we run groups of methods of protocol NSFetchedResultsControllerDelegate to refresh tableView.
Method
 (NSIndexPath *)tableView:(UITableView *)tableView
willSelectRowAtIndexPath:(NSIndexPath*)indexPath;
is called after didSelect and can confirm, cancel, change user selection.
TypeSelectionViewController.
It allows user to select category of diches. It’s a checklist – TableView that can save checked states of cells
(we have only one cell).
For this TableView feature set property accessoryType = UITableViewCellAccessoryCheckmark:
cell.accessoryType = UITableViewCellAccessoryCheckmark.
To cancel check mode set accessoryType = UITableViewCellAccessoryNone:
cell.accessoryType = UITableViewCellAccessoryNone.
Following three rows are declaration of anonymous category:
@interface TypeSelectionViewController()
@property (nonatomic, retain) NSArray *recipeTypes;
@end
33
TypeSelectionViewController.h
@class Recipe;
@interface TypeSelectionViewController : UITableViewController {
@private
Recipe *recipe;
NSArray *recipeTypes;
}
@property (nonatomic, retain) Recipe *recipe;
@property (nonatomic, retain, readonly) NSArray *recipeTypes;
@end
TypeSelectionViewController.m
#import "TypeSelectionViewController.h"
#import "Recipe.h"
@interface TypeSelectionViewController()
@property (nonatomic, retain) NSArray *recipeTypes;
@end
@implementation TypeSelectionViewController
@synthesize recipe;
@synthesize recipeTypes;
#pragma mark #pragma mark UIViewController
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Fetch the recipe types in alphabetical order by name from the recipe's context.
NSManagedObjectContext *context = [recipe managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:@"RecipeType" inManagedObjectContext:context]];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:&sortDescriptor count:1];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError *error = nil;
NSArray *types = [context executeFetchRequest:fetchRequest error:&error];
self.recipeTypes = types;
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
#pragma mark #pragma mark UITableView Delegate/Datasource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Number of rows is the number of recipe types
return [recipeTypes count];
}
34
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease];
}
// Configure the cell
NSManagedObject *recipeType = [recipeTypes objectAtIndex:indexPath.row];
cell.textLabel.text = [recipeType valueForKey:@"name"];
if (recipeType == recipe.type) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// If there was a previous selection, unset the accessory view for its cell.
NSManagedObject *currentType = recipe.type;
if (currentType != nil) {
NSInteger index = [recipeTypes indexOfObject:currentType];
NSIndexPath *selectionIndexPath = [NSIndexPath indexPathForRow:index inSection:0];
UITableViewCell *checkedCell = [tableView cellForRowAtIndexPath:selectionIndexPath];
checkedCell.accessoryType = UITableViewCellAccessoryNone;
}
// Set the checkmark accessory for the selected row.
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryCheckmark];
// Update the type of the recipe instance
recipe.type = [recipeTypes objectAtIndex:indexPath.row];
// Deselect the row.
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (void)dealloc {
[recipe release];
[recipeTypes release];
[super dealloc];
}
@end
InstructionsViewController.
Allows to review, edit, create description of recipe.
Main element is UITextView. Copy .h and .m files, create and open InstructionsView.xib.
Drag and drop new elements on View:
 Label - Recipe Name;
 Text View.
Click File's Owner in the Placeholders bar, set its class - InstructionsViewController .
On the Connection Inspector tab link property to appropriate objects in the Objects bar.
In .m file we can see self.editButtonItem and method “setEditing: animated:” again.
InstructionsViewController.h
@class Recipe;
35
@interface InstructionsViewController : UIViewController {
@private
Recipe *recipe;
UITextView *instructionsText;
UILabel *nameLabel;
}
@property (nonatomic, retain) Recipe *recipe;
@property (nonatomic, retain) IBOutlet UITextView *instructionsText;
@property (nonatomic, retain) IBOutlet UILabel *nameLabel;
@end
InstructionsViewController.m
#import "InstructionsViewController.h"
#import "Recipe.h"
@implementation InstructionsViewController
@synthesize recipe;
@synthesize instructionsText;
@synthesize nameLabel;
- (void)viewDidLoad {
UINavigationItem *navigationItem = self.navigationItem;
navigationItem.title = @"Instructions";
self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
- (void)viewDidUnload {
self.instructionsText = nil;
self.nameLabel = nil;
[super viewDidUnload];
}
- (void)viewWillAppear:(BOOL)animated {
// Update the views appropriately
nameLabel.text = recipe.name;
instructionsText.text = recipe.instructions;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Support all orientations except upside-down
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
instructionsText.editable = editing;
[self.navigationItem setHidesBackButton:editing animated:YES];
/*
If editing is finished, update the recipe's instructions and save the managed object context.
*/
if (!editing) {
recipe.instructions = instructionsText.text;
NSManagedObjectContext *context = recipe.managedObjectContext;
NSError *error = nil;
if (![context save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
*/
36
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}
- (void)dealloc {
[recipe release];
[instructionsText release];
[nameLabel release];
[super dealloc];
}
@end
RecipePhotoViewController.
This is a simple ViewController for review one image. We create and add UIImageView in code (not in .xib)
in the method “loadView”.
RecipePhotoViewController.h
@class Recipe;
@interface RecipePhotoViewController : UIViewController {
@private
Recipe *recipe;
UIImageView *imageView;
}
@property(nonatomic, retain) Recipe *recipe;
@property(nonatomic, retain) UIImageView *imageView;
@end
RecipePhotoViewController.m
#import "RecipePhotoViewController.h"
#import "Recipe.h"
@implementation RecipePhotoViewController
@synthesize recipe;
@synthesize imageView;
- (void)loadView {
self.title = @"Photo";
imageView = [[UIImageView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
imageView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.backgroundColor = [UIColor blackColor];
self.view = imageView;
}
- (void)viewWillAppear:(BOOL)animated {
imageView.image = [recipe.image valueForKey:@"image"];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (void)dealloc {
37
[imageView release];
[recipe release];
[super dealloc];
}
@end
IngredientDetailViewController.
It allows add/edit pair ingredient/amount. It includes two cells (name/amount) and to buttons (save/cancel).
IngredientDetailViewController.h
@class Recipe, Ingredient, EditingTableViewCell;
@interface IngredientDetailViewController : UITableViewController {
@private
Recipe *recipe;
Ingredient *ingredient;
EditingTableViewCell *editingTableViewCell;
}
@property (nonatomic, retain) Recipe *recipe;
@property (nonatomic, retain) Ingredient *ingredient;
@property (nonatomic, assign) IBOutlet EditingTableViewCell *editingTableViewCell;
@end
IngredientDetailViewController.m
#import "IngredientDetailViewController.h"
#import "Recipe.h"
#import "Ingredient.h"
#import "EditingTableViewCell.h"
@implementation IngredientDetailViewController
@synthesize recipe, ingredient, editingTableViewCell;
#pragma mark #pragma mark View controller
- (id)initWithStyle:(UITableViewStyle)style {
if (self = [super initWithStyle:style]) {
UINavigationItem *navigationItem = self.navigationItem;
navigationItem.title = @"Ingredient";
UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self action:@selector(cancel:)];
self.navigationItem.leftBarButtonItem = cancelButton;
[cancelButton release];
UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave
target:self action:@selector(save:)];
self.navigationItem.rightBarButtonItem = saveButton;
[saveButton release];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.allowsSelection = NO;
self.tableView.allowsSelectionDuringEditing = NO;
38
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
#pragma mark #pragma mark Table view
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 2;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *IngredientsCellIdentifier = @"IngredientsCell";
EditingTableViewCell *cell = (EditingTableViewCell *)[tableView dequeueReusableCellWithIdentifier:IngredientsCellIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:@"EditingTableViewCell" owner:self options:nil];
cell = editingTableViewCell;
self.editingTableViewCell = nil;
}
if (indexPath.row == 0) {
cell.label.text = @"Ingredient";
cell.textField.text = ingredient.name;
cell.textField.placeholder = @"Name";
}
else if (indexPath.row == 1) {
cell.label.text = @"Amount";
cell.textField.text = ingredient.amount;
cell.textField.placeholder = @"Amount";
}
return cell;
}
#pragma mark #pragma mark Save and cancel
- (void)save:(id)sender {
NSManagedObjectContext *context = [recipe managedObjectContext];
/*
If there isn't an ingredient object, create and configure one.
*/
if (!ingredient) {
self.ingredient = [NSEntityDescription insertNewObjectForEntityForName:@"Ingredient" inManagedObjectContext:context];
[recipe addIngredientsObject:ingredient];
ingredient.displayOrder = [NSNumber numberWithInteger:[recipe.ingredients count]];
}
/*
Update the ingredient from the values in the text fields.
*/
EditingTableViewCell *cell;
cell = (EditingTableViewCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
ingredient.name = cell.textField.text;
cell = (EditingTableViewCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]];
ingredient.amount = cell.textField.text;
/*
39
Save the managed object context.
*/
NSError *error = nil;
if (![context save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping
application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that
instructs the user to quit the application by pressing the Home button.
*/
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
[self.navigationController popViewControllerAnimated:YES];
}
- (void)cancel:(id)sender {
[self.navigationController popViewControllerAnimated:YES];
}
#pragma mark #pragma mark Memory management
- (void)dealloc {
[recipe release];
[ingredient release];
[super dealloc];
}
@end
Create class EditingTableViewCell inherits UITableViewCell, add properties UILabel and UITextField.
Create empty EditingTableViewCell.xib and open it. Drag and drop new
TableViewCell on View. Set on Identity Inspector tab:
 for File's Owner – class IngredientDetailViewController;
 for Table View Cell – class EditingTableViewCell.
Drag and drop new elements on TableViewCell:
 UILabel;
 UITextField.
Set on Attribute Inspector tab for UITextField:
 Alignment = left
 Border Style = the first left
 Background = default
 Drowing – opaque, autoresize subview
 font = 14;
for UILabel:
 Alignment = right
 Background = default
 font = 13.
On the Connections Inspector tab:
 for File's Owner - link property editingTableViewCell to object Editing Table View Cell;
 for Editing Table View Cell – link property label and textField to appropriate objects;
 for UITextField – link its delegate to File's Owner.
EditingTableViewCell.h
@interface EditingTableViewCell : UITableViewCell {
UILabel *label;
40
UITextField *textField;
}
@property (nonatomic, retain) IBOutlet UILabel *label;
@property (nonatomic, retain) IBOutlet UITextField *textField;
@end
EditingTableViewCell.m
#import "EditingTableViewCell.h"
@implementation EditingTableViewCell
@synthesize label, textField;
- (void)dealloc {
[label release];
[textField release];
[super dealloc];
}
@end
TemperatureConverterViewController.
This is a ViewController with TableView. There are 3 columns in the table.
Create class. Copy files. Pay attention to method “temperatureData”. Data for tableview is saved in a
“property list”. In “temperatureData” we get data from file and use it for array initialization.You can write
data to file manually or use Property List Editor (New File → Resource → Property List). This topic is
beyond the scope of this article.
Create and open TemperatureConverter.xib. Drag and drop 3 new UILabels on View, set text for every
UILabel. Drag and drop a new TableView above UILabels. Set class names, link IBOutlets.
TemperatureConverterViewController.h
@class TemperatureCell;
@interface TemperatureConverterViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
NSArray *temperatureData;
UITableView *tableView;
TemperatureCell *temperatureCell;
}
@property (nonatomic, retain) NSArray *temperatureData;
@property (nonatomic, retain) IBOutlet UITableView *tableView;
@property (nonatomic, retain) IBOutlet TemperatureCell *temperatureCell;
@end
TemperatureConverterViewController.m
#import "TemperatureConverterViewController.h"
#import "TemperatureCell.h"
@implementation TemperatureConverterViewController
@synthesize temperatureData;
@synthesize tableView, temperatureCell;
#pragma mark #pragma mark View lifecycle
- (void)viewDidLoad {
self.title = @"Temperature";
self.tableView.allowsSelection = NO;
}
41
- (void)viewDidUnload {
self.tableView = nil;
[super viewDidUnload];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#pragma mark #pragma mark Tableview datasource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.temperatureData count];
}
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";
// Create a new TemperatureCell if necessary
TemperatureCell *cell = (TemperatureCell *)[tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:@"TemperatureCell" owner:self options:nil];
cell = temperatureCell;
self.temperatureCell = nil;
}
// Configure the temperature cell with the relevant data
NSDictionary *temperatureDictionary = [self.temperatureData objectAtIndex:indexPath.row];
[cell setTemperatureDataFromDictionary:temperatureDictionary];
return cell;
}
#pragma mark #pragma mark Temperature data
- (NSArray *)temperatureData {
if (temperatureData == nil) {
// Get the temperature data from the TemperatureData property list.
NSString *temperatureDataPath = [[NSBundle mainBundle] pathForResource:@"TemperatureData" ofType:@"plist"];
NSArray *array = [[NSArray alloc] initWithContentsOfFile:temperatureDataPath];
self.temperatureData = array;
[array release];
}
return temperatureData;
}
#pragma mark #pragma mark Memory management
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
self.temperatureData = nil;
}
- (void)dealloc {
[tableView release];
[temperatureData release];
[super dealloc];
}
42
@end
Create TemperatureCell inherits UITableViewCell, copy files. Create and open TemperatureCell.xib. Drag
and drop a new TableViewCell on View, drag and drop 3 new UILabels on it. Set class names, link objects to
IBOutlets.
TemperatureCell.h
@interface TemperatureCell : UITableViewCell {
UILabel *cLabel;
UILabel *fLabel;
UILabel *gLabel;
}
@property (nonatomic, retain) IBOutlet UILabel *cLabel;
@property (nonatomic, retain) IBOutlet UILabel *fLabel;
@property (nonatomic, retain) IBOutlet UILabel *gLabel;
- (void)setTemperatureDataFromDictionary:(NSDictionary *)temperatureDictionary;
@end
TemperatureCell.m
#import "TemperatureCell.h"
@implementation TemperatureCell
@synthesize cLabel, fLabel, gLabel;
- (void)setTemperatureDataFromDictionary:(NSDictionary *)temperatureDictionary {
// Update text in labels from the dictionary
cLabel.text = [temperatureDictionary objectForKey:@"c"];
fLabel.text = [temperatureDictionary objectForKey:@"f"];
gLabel.text = [temperatureDictionary objectForKey:@"g"];
}
- (void)dealloc {
[cLabel release];
[fLabel release];
[gLabel release];
[super dealloc];
}
@end
WeightConverterViewController.
It allows to convert kilogramms to pounds and vice versa. We use two Pickers for set values:
ImperialPickerController and MetricPickerController. Both of them inherit NSObject and implement
protocols UIPickerViewDataSource, UIPickerViewDelegate. Create classes, copy files.
ImperialPickerController.h
@interface ImperialPickerController : NSObject <UIPickerViewDataSource, UIPickerViewDelegate> {
UIPickerView *pickerView;
UILabel *label;
}
@property (nonatomic, retain) IBOutlet UIPickerView *pickerView;
@property (nonatomic, retain) IBOutlet UILabel *label;
- (void)updateLabel;
43
@end
ImperialPickerController.m
#import "ImperialPickerController.h"
@implementation ImperialPickerController
// Identifiers and widths for the various components
#define POUNDS_COMPONENT 0
#define POUNDS_COMPONENT_WIDTH 110
#define POUNDS_LABEL_WIDTH 60
#define OUNCES_COMPONENT 1
#define OUNCES_COMPONENT_WIDTH 106
#define OUNCES_LABEL_WIDTH 56
// Identifies for component views
#define VIEW_TAG 41
#define SUB_LABEL_TAG 42
#define LABEL_TAG 43
@synthesize pickerView;
@synthesize label;
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 2;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
// Number of rows depends on the currently-selected unit and the component.
if (component == POUNDS_COMPONENT) {
return 29;
}
// OUNCES_LABEL_COMPONENT
return 16;
}
- (UIView *)labelCellWithWidth:(CGFloat)width rightOffset:(CGFloat)offset {
// Create a new view that contains a label offset from the right.
CGRect frame = CGRectMake(0.0, 0.0, width, 32.0);
UIView *view = [[[UIView alloc] initWithFrame:frame] autorelease];
view.tag = VIEW_TAG;
frame.size.width = width - offset;
UILabel *subLabel = [[UILabel alloc] initWithFrame:frame];
subLabel.textAlignment = UITextAlignmentRight;
subLabel.backgroundColor = [UIColor clearColor];
subLabel.font = [UIFont systemFontOfSize:24.0];
subLabel.userInteractionEnabled = NO;
subLabel.tag = SUB_LABEL_TAG;
[view addSubview:subLabel];
[subLabel release];
return view;
}
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component
reusingView:(UIView *)view {
44
UIView *returnView = nil;
// Reuse the label if possible, otherwise create and configure a new one.
if ((view.tag == VIEW_TAG) || (view.tag == LABEL_TAG)) {
returnView = view;
}
else {
if (component == POUNDS_COMPONENT) {
returnView = [self labelCellWithWidth:POUNDS_COMPONENT_WIDTH rightOffset:POUNDS_LABEL_WIDTH];
}
else {
returnView = [self labelCellWithWidth:OUNCES_COMPONENT_WIDTH rightOffset:OUNCES_LABEL_WIDTH];
}
}
// The text shown in the component is just the number of the component.
NSString *text = [NSString stringWithFormat:@"%d", row];
// Where to set the text in depends on what sort of view it is.
UILabel *theLabel = nil;
if (returnView.tag == VIEW_TAG) {
theLabel = (UILabel *)[returnView viewWithTag:SUB_LABEL_TAG];
}
else {
theLabel = (UILabel *)returnView;
}
theLabel.text = text;
return returnView;
}
- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
if (component == POUNDS_COMPONENT) {
return POUNDS_COMPONENT_WIDTH;
}
// OUNCES_COMPONENT
return OUNCES_COMPONENT_WIDTH;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
// If the user chooses a new row, update the label accordingly.
[self updateLabel];
}
- (void)updateLabel {
/*
If the user has entered imperial units, find the number of pounds and ounces and convert that to kilograms and grams.
Don't display 0 kg.
*/
NSInteger ounces = [pickerView selectedRowInComponent:OUNCES_COMPONENT];
ounces += [pickerView selectedRowInComponent:POUNDS_COMPONENT] * 16;
float grams = ounces * 28.349;
if (grams > 1000.0) {
NSInteger kg = grams / 1000;
grams -= kg *1000;
label.text = [NSString stringWithFormat:@"%d kg %1.0f g", kg, grams];
}
else {
label.text = [NSString stringWithFormat:@"%1.0f g", grams];
}
}
- (void)dealloc {
45
[pickerView release];
[label release];
[super dealloc];
}
@end
MetricPickerController.h
@interface MetricPickerController : NSObject <UIPickerViewDataSource, UIPickerViewDelegate> {
UIPickerView *pickerView;
UILabel *label;
}
@property (nonatomic, retain) IBOutlet UIPickerView *pickerView;
@property (nonatomic, retain) IBOutlet UILabel *label;
- (UIView *)viewForComponent:(NSInteger)component;
- (void)updateLabel;
@end
MetricPickerController.m
#import "MetricPickerController.h"
@implementation MetricPickerController
// Identifiers and widths for the various components
#define KG_COMPONENT 0
#define KG_COMPONENT_WIDTH 88
#define KG0_LABEL_WIDTH 46
#define G0_COMPONENT 3
#define G0_COMPONENT_WIDTH 74
#define G0_LABEL_WIDTH 44
#define G_COMPONENT_WIDTH 50
// Identifies for component views
#define VIEW_TAG 41
#define SUB_LABEL_TAG 42
#define LABEL_TAG 43
@synthesize pickerView;
@synthesize label;
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 4;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
// Number of rows depends on the currently-selected unit and the component.
if (component == KG_COMPONENT) {
return 20;
}
return 10;
}
- (UIView *)labelCellWidth:(CGFloat)width rightOffset:(CGFloat)offset {
// Create a new view that contains a label offset from the right.
CGRect frame = CGRectMake(0.0, 0.0, width, 32.0);
UIView *view = [[[UIView alloc] initWithFrame:frame] autorelease];
view.tag = VIEW_TAG;
46
frame.size.width = width - offset;
UILabel *subLabel = [[UILabel alloc] initWithFrame:frame];
subLabel.textAlignment = UITextAlignmentRight;
subLabel.backgroundColor = [UIColor clearColor];
subLabel.font = [UIFont systemFontOfSize:24.0];
subLabel.userInteractionEnabled = NO;
subLabel.tag = SUB_LABEL_TAG;
[view addSubview:subLabel];
[subLabel release];
return view;
}
- (UIView *)viewForComponent:(NSInteger)component {
/*
Return a view appropriate for the specified picker view and component.
If it's the picker view, or if it's the kg or g component of the metric view, create a UIView that contains a label. The label can
then be offset in the containing view so that its text does not overlap the unit symbol.
For the remaining components, simple create a label to contain the text.
Give all the views tags so they can be idntified easily.
*/
if (component == KG_COMPONENT) {
return [self labelCellWidth:KG_COMPONENT_WIDTH rightOffset:KG0_LABEL_WIDTH];
}
if (component == G0_COMPONENT) {
return [self labelCellWidth:G0_COMPONENT_WIDTH rightOffset:G0_LABEL_WIDTH];
}
CGRect frame = CGRectMake(0.0, 0.0, 36.0, 32.0);
UILabel *aLabel = [[[UILabel alloc] initWithFrame:frame] autorelease];
aLabel.textAlignment = UITextAlignmentCenter;
aLabel.backgroundColor = [UIColor clearColor];
aLabel.font = [UIFont systemFontOfSize:24.0];
aLabel.userInteractionEnabled = NO;
aLabel.tag = LABEL_TAG;
return aLabel;
}
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component
reusingView:(UIView *)view {
UIView *returnView = nil;
// Reuse the label if possible, otherwise create and configure a new one.
if ((view.tag == VIEW_TAG) || (view.tag == LABEL_TAG)) {
returnView = view;
}
else {
returnView = [self viewForComponent:component];
}
// The text shown in the component is just the number of the component.
NSString *text = [NSString stringWithFormat:@"%d", row];
// Where to set the text in depends on what sort of view it is.
UILabel *theLabel = nil;
if (returnView.tag == VIEW_TAG) {
theLabel = (UILabel *)[returnView viewWithTag:SUB_LABEL_TAG];
}
else {
theLabel = (UILabel *)returnView;
}
47
theLabel.text = text;
return returnView;
}
- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
// The width of the component depends on the currently-selected unit and the component.
if (component == KG_COMPONENT) {
return KG_COMPONENT_WIDTH;
}
if (component == G0_COMPONENT) {
return G0_COMPONENT_WIDTH;
}
return G_COMPONENT_WIDTH;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
// If the user chooses a new row, update the label accordingly.
[self updateLabel];
}
- (void)updateLabel {
/*
If the user has entered metric units, find the number of grams and convert that to pounds and ounces.
Don't display 0 lbs; round 15.95 ounces up to 1 lb, and use NSDecimalNumberHandler to round ounces for a more attractive
display.
*/
static NSDecimalNumberHandler* roundingBehavior = nil;
if (roundingBehavior == nil) {
roundingBehavior =
[[NSDecimalNumberHandler alloc] initWithRoundingMode:NSRoundPlain scale:1 raiseOnExactness:NO raiseOnOverflow:NO
raiseOnUnderflow:NO raiseOnDivideByZero:NO];
}
NSInteger grams = 0;
grams += [pickerView selectedRowInComponent:3];
grams += [pickerView selectedRowInComponent:2] * 10;
grams += [pickerView selectedRowInComponent:1] * 100;
grams += [pickerView selectedRowInComponent:0] * 1000;
NSDecimalNumber *ouncesDecimal;
NSDecimalNumber *roundedOunces;
float ounces = grams / 28.349;
if (ounces >= 15.95) {
NSInteger lbs = ounces / 16;
ounces -= lbs * 16;
if (ounces >= 15.95) {
ounces = 0;
lbs += 1;
}
ouncesDecimal = [[NSDecimalNumber alloc] initWithFloat:ounces];
roundedOunces = [ouncesDecimal decimalNumberByRoundingAccordingToBehavior:roundingBehavior];
[ouncesDecimal release];
label.text = [NSString stringWithFormat:@"%d lbs %@ oz", lbs, roundedOunces];
}
else {
ouncesDecimal = [[NSDecimalNumber alloc] initWithFloat:ounces];
roundedOunces = [ouncesDecimal decimalNumberByRoundingAccordingToBehavior:roundingBehavior];
[ouncesDecimal release];
label.text = [NSString stringWithFormat:@"%@ oz", roundedOunces];
}
48
}
- (void)dealloc {
[pickerView release];
[label release];
[super dealloc];
}
@end
Create class WeightConverterViewController. Copy file.
WeightConverterViewController.h
@class MetricPickerController;
@class ImperialPickerController;
@interface WeightConverterViewController : UIViewController {
UIView *pickerViewContainer;
MetricPickerController *metricPickerController;
UIView *metricPickerViewContainer;
UIView *imperialPickerViewContainer;
ImperialPickerController *imperialPickerController;
UISegmentedControl *segmentedControl;
NSUInteger selectedUnit;
}
@property (nonatomic, retain) IBOutlet UIView *pickerViewContainer;
@property (nonatomic, retain) IBOutlet MetricPickerController *metricPickerController;
@property (nonatomic, retain) IBOutlet UIView *metricPickerViewContainer;
@property (nonatomic, retain) IBOutlet ImperialPickerController *imperialPickerController;
@property (nonatomic, retain) IBOutlet UIView *imperialPickerViewContainer;
@property (nonatomic, retain) IBOutlet UISegmentedControl *segmentedControl;
- (IBAction)toggleUnit;
@end
WeightConverterViewController.m
#import "WeightConverterViewController.h"
#import "MetricPickerController.h"
#import "ImperialPickerController.h"
@implementation WeightConverterViewController
@synthesize pickerViewContainer;
@synthesize imperialPickerController;
@synthesize imperialPickerViewContainer;
@synthesize metricPickerController;
@synthesize metricPickerViewContainer;
@synthesize segmentedControl;
#define METRIC_INDEX 0
#define IMPERIAL_INDEX 1
- (void)viewDidLoad {
[super viewDidLoad];
49
self.navigationItem.title = @"Weight";
// Set the currently-selected unit for self and the segmented control
selectedUnit = METRIC_INDEX;
segmentedControl.selectedSegmentIndex = selectedUnit;
[self toggleUnit];
}
- (void)viewDidUnload {
self.pickerViewContainer = nil;
self.metricPickerController = nil;
self.metricPickerViewContainer = nil;
self.imperialPickerController = nil;
self.imperialPickerViewContainer = nil;
self.segmentedControl = nil;
[super viewDidUnload];
}
- (IBAction)toggleUnit {
/*
When the user changes the selection in the segmented control, set the appropriate picker as the current subview of the picker
container view (and remove the previous one).
*/
selectedUnit = [segmentedControl selectedSegmentIndex];
if (selectedUnit == IMPERIAL_INDEX) {
[metricPickerViewContainer removeFromSuperview];
[pickerViewContainer addSubview:imperialPickerViewContainer];
[imperialPickerController updateLabel];
} else {
[imperialPickerViewContainer removeFromSuperview];
[pickerViewContainer addSubview:metricPickerViewContainer];
[metricPickerController updateLabel];
}
}
- (void)dealloc {
[pickerViewContainer release];
[imperialPickerController release];
[imperialPickerViewContainer release];
[metricPickerController release];
[metricPickerViewContainer release];
[segmentedControl release];
[super dealloc];
}
@end
Create WeightConverter.xib (User Interface → View) and open it. Drag and drop one new View (size
320x113) at the bottom and another View (size 320x216) above the first. The second view will includes two
pickers. Drag and drop new UIButton at the top part. Drag and drop a new UILabel onto UIButton and set it
the same size. Put on Segmented Control above UIButton. UISegmentedControl is horizontal multibutton
element. In this case, there are two buttons on it. They will switch between two pickers For SegmentControl
on the Attribute Inspector tab set property Segments = 2, set property Segment:

for Segment 0: title = Metric,

for Segment 1: title = Imperial.
50
51
Figure 11
52
Drag and drop 2 new Objects and 2 new Views to the Objects bar. Pay attention they must be on the same
lavel hierarchy with first View. Both new Views should be placed exactly over central subview (size
320x216). For Objects set classes: ImperialPickerController and MetricPickerController. For View set
names - Metric Container and
Imperial Container.
Figure 12
Into the tree of Metric Container drag and drop a new PickerView the same size and 2 new UILabels over it.
Set texts of UILabels = “kg” and “g”.
53
Figure 13
Do the same thing with Imperial Container. Set texts of UILabels = “lbs” and “oz”.
Set class File's Owner – WeightConverterViewController. On the Connections Inspector tab - link IBOutlets
to appropriate Objects – Views to View, ViewControllers to ViewControllers, segmentedControl to
segmentedControl, property UIView *pickerViewContainer to central View with size 320x216.
54
Figure 14
Figure 15
For MetricPickerController link pickerView to appropriate Picker, and label – to Label in the first View.
55
Do the same for ImperialPickerController.
For Picker in Metric Container set delegate and dataSource = MetricPickerController.
For Picker in Imperial Container set delegate and dataSource = ImperialPickerController.
For SegmentedControl link event “Value Changed” to handler = toggleUnit.
56