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
© Copyright 2024