Quantcast
Channel: Envato Tuts+ Code - Mobile Development
Viewing all articles
Browse latest Browse all 1836

Building a Shopping List Application From Scratch – Part 2

$
0
0

In the previous lessen, we laid the foundation for the shopping list application. In the first part of this lesson, we will further refine the application by enabling the user to edit and delete items from the list. In the second part of this lesson, we add the ability to select items from the list to create the shopping list.


Outline

As I mentioned in the previous article, one of the core features of the shopping list application is managing a list of items. We have done a great job so far, but the user should have the ability to edit and delete items from the list, which will be the focus of the first part of this tutorial. In the second part, we will implement the second core feature of the shopping list application, creating a shopping list by selecting items from the list of items.


Step 8: Deleting Items

Deleting items from the list is an important addition in terms of user experience and overall usability. Adding this ability involves (1) removing the item from the view controller’s items property (the table view’s data source), (2) updating the table view, and (3) saving the changes to disk. Let’s see how this works in practice.

We first need to add an edit button to the navigation bar. In the view controller’s viewDidLoad method, create an instance of UIBarButtonItem and assign it to the rightBarButtonItem property of the view controller’s navigationItem property. As in the previous lesson, we create a bar button item by calling the initWithBarButtonSystemItem:target:action: initialization method. The first parameter that we pass is UIBarButtonSystemItemEdit. In addition, we pass self as the target and set the action to editItems:.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Create Add Button
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addItem:)];
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:@selector(editItems:)];
    NSLog(@"Items > %@", self.items);
}

The implementation of the editItems: action is only one line of code as you can see below. Whenever the user taps the edit button, the table view is toggled into or out of edit mode. We do this by using a little trick. We ask the table view whether it currently is in edit mode, which returns a BOOL value, and inverse the returned value (YES becomes NO and vice versa). The method we call on the table view is setEditing:animated:, which is a specialized setter that accepts an animation parameter.

- (void)editItems:(id)sender {
    [self.tableView setEditing:![self.tableView isEditing] animated:YES];
}

If you run the shopping list application in the iOS Simulator and tap the edit button, you should see the table view being toggled into and out of edit mode (figure 1).

Building a Groceries Application From Scratch - Part 2 - Toggling the Table View Into and Out Of Edit Mode - Figure 1

To methods of the UITableViewDataSource protocol are important for enabling editing in a table view, (1) tableView:canEditRowAtIndexPath: and (2) tableView:commitEditingStyle:forRowAtIndexPath:.

When the user taps the edit button, the table view asks its data source which rows are editable by sending the data source a message of tableView:canEditRowAtIndexPath:. When YES is returned for a particular index path, the table view instructs the table view cell of the corresponding row that it needs to toggle into or out of edit mode, depending on the table view’s edit mode. In practice, this means that the table view cell shows or hides its editing controls. This concept is best understand by implementing the tableView:canEditRowAtIndexPath: method as shown below.

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    if ([indexPath row] == 1) {
        return NO;
    }
    return YES;
}

The above implementation of tableView:canEditRowAtIndexPath: enables the user to edit every row in the table view except for the second row. Run the application in the iOS Simulator and give it a try (figure 2).

Building a Groceries Application From Scratch - Part 2 - Toggling the Table View Into and Out Of Edit Mode - Figure 2

For the shopping list application, the user needs to be able to edit every row in the table view, which means that tableView:canEditRowAtIndexPath: should always return YES as illustrated below.

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

You may have noticed that nothing happens when you try to delete a row in the table view. The puzzle isn’t finished yet. Whenever the user taps the delete button of a row, the table view sends its data source a message of tableView:commitEditingStyle:forRowAtIndexPath:. The second argument of the corresponding method denotes what type of action the user has performed, that is, inserting or deleting a row. The shopping list application will only provide support for deleting rows from the table view.

Deleting rows from a table view involves (1) deleting the corresponding item from the data source, the view controller’s items property, and (2) updating the table view by deleting the corresponding row.

Let’s inspect the implementation of tableView:commitEditingStyle:forRowAtIndexPath:. The method starts by checking whether the editing style is equal to UITableViewCellEditingStyleDelete as we only want to allow the user to delete rows from the table view. If the editing style is equal to UITableViewCellEditingStyleDelete, then (1) the corresponding item is removed from the items array, (2) the corresponding row is deleted from the table view, and (3) the updated list of items is saved to disk.

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete Item from Items
        [self.items removeObjectAtIndex:[indexPath row]];
        // Update Table View
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
        // Save Changes to Disk
        [self saveItems];
    }
}

Build and run the project to test the delete functionality. Don’t forget to quit and relaunch the application to verify that items are permanently deleted from the list.


Step 9: Editing Items

We could reuse the MTAddItemViewController class for editing items, but reusing a view controller class for both adding and editing items can often overcomplicate the implementation so much that I generally end up creating a separate UIViewController subclass for editing items. This will initially result in some overlap in terms of implementation and user interface, but it will give us more flexibility down the road.

Creating the Editing View Controller

Create a new UIViewController subclass, name it MTEditItemViewController, and tell Xcode to create a nib file for the class by checking the checkbox labeled With XIB for user interface. The header files of the MTAddItemViewController and MTEditItemViewController classes are very similar.

I have included the header file (without comments) of the MTEditItemViewController class below. After importing the headers of the UIKit framework, a forward class and protocol declaration is added. The interface of the class is similar to that of the MTAddItemViewController class. The main difference is the declaration of a custom initialization method, initWithItem:andDelegate:. Even though this isn’t strictly necessary, I would like to show you the advantages of creating custom initialization methods.

The header file ends with the declaration of the MTEditItemViewControllerDelegate protocol. The protocol declaration contains one method that is called when an item is updated. The method accepts the view controller and the updated item as its arguments.

#import <UIKit/UIKit.h>
@class MTItem;
@protocol MTEditItemViewControllerDelegate;
@interface MTEditItemViewController : UIViewController
@property IBOutlet UITextField *nameTextField;
@property IBOutlet UITextField *priceTextField;
#pragma mark -
#pragma mark Initialization
- (id)initWithItem:(MTItem *)item andDelegate:(id<MTEditItemViewControllerDelegate>)delegate;
@end
@protocol MTEditItemViewControllerDelegate <NSObject>
- (void)controller:(MTEditItemViewController *)controller didUpdateItem:(MTItem *)item;
@end

Even though we added a forward class declaration to the header file of the MTEditItemViewController class, we still need to import the header file of the MTItem class in the implementation file. In the class extension of MTEditItemViewController, we declare the item and delegate properties. The difference with the MTAddItemViewController class is that the delegate property is private in the MTEditItemViewController class.

#import "MTEditItemViewController.h"
#import "MTItem.h"
@interface MTEditItemViewController ()
@property MTItem *item;
@property (weak) id<MTEditItemViewControllerDelegate> delegate;
@end

How can we set the delegate of an instance of the MTEditItemViewController class if it is private? The item and delegate properties are assigned using the custom initialization method. Not only will this keep the code more concise and readable, it will also prevent direct access to the item and delegate properties. Let’s explore the initWithItem:andDelegate: method.

The implementation of initWithItem:andDelegate: starts by invoking initWithNibName:bundle: on super, the superclass of MTEditItemViewController. The remainder of the method implementation is trivial, that is, we set the item and delegate properties with the values passed to the method.

- (id)initWithItem:(MTItem *)item andDelegate:(id<MTEditItemViewControllerDelegate>)delegate {
    self = [super initWithNibName:@"MTEditItemViewController" bundle:nil];
    if (self) {
        // Set Item
        self.item = item;
        // Set Delegate
        self.delegate = delegate;
    }
    return self;
}

Open MTEditItemViewController.xib to create the user interface. There is no need to add a navigation bar to the view controller’s view as the view controller will be the child of a navigation controller, which means that we get the navigation bar for free. We will add a save button in the class’ viewDidLoad method a bit later.

Drag two UITextField instances from the Object Library to the view controller’s view and position them as shown in the figure below (figure 3). Select the top text field, open the Attributes Inspector, and enter Name in the Placeholder field. Select the bottom text field and, in the Attributes Inspector, set its placeholder text to Price and set the Keyboard setting to Number Pad. Select the File’s Owner object, open the Connections Inspector, and connect the nameTextField and priceTextField outlets to the corresponding text field in the user interface.

Building a Shopping List Application From Scratch - Part 2 - Creating the User Interface of the MTEditItemViewController class - Figure 3

In the view controller’s viewDidLoad method, create the save button as we did in the MTAddItemViewController class (see below).

- (void)viewDidLoad {
    [super viewDidLoad];
    // Create Save Button
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(save:)];
}

The implementation of the save: action is very similar to the one we implemented in the MTAddItemViewController class. However, there are a few subtle differences that I want to point out. Instead of passing the name and price values to the delegate, we directly update the item and pass the updated item to the view controller’s delegate. Because the view controller is a child view controller of a navigation controller, we dismiss the view controller by popping it of the navigation stack.

- (void)save:(id)sender {
    NSString *name = [self.nameTextField text];
    float price = [[self.priceTextField text] floatValue];
    // Update Item
    [self.item setName:name];
    [self.item setPrice:price];
    // Notify Delegate
    [self.delegate controller:self didUpdateItem:self.item];
    // Pop View Controller
    [self.navigationController popViewControllerAnimated:YES];
}

Showing the Edit Item View Controller

In a few minutes, we will implement the functionality that allows the user to select items to add them to the shopping list. The user will be able to do this by tapping a row in the list view. The question then arises how the user will be able to edit an item if tapping a row is reserved for adding an item to the shopping list?

The UIKit framework provides a detail disclosure button for exactly this use case. A detail disclosure button is a small, blue chevron positioned at the right of a table view cell. To add a detail disclosure button to a table view cell, we need to revisit the tableView:cellForRowAtIndexPath: method and amend it as shown below. Every table view cell has a accessoryType property. In the implementation below, we set it to UITableViewCellAccessoryDetailDisclosureButton. As you may have noticed, the engineers at Apple don’t like short names.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell Identifier";
    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    // Fetch Item
    MTItem *item = [self.items objectAtIndex:[indexPath row]];
    // Configure Cell
    [cell.textLabel setText:[item name]];
    [cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
    return cell;
}

How does the table view notify the delegate when the detail disclosure button is tapped? Unsurprisingly, the UITableViewDelegate protocol defines the tableView:accessoryButtonTappedForRowWithIndexPath: method for this purpose. Take a look at its implementation.

- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
    // Fetch Item
    MTItem *item = [self.items objectAtIndex:[indexPath row]];
    // Initialize Edit Item View Controller
    MTEditItemViewController *editItemViewController = [[MTEditItemViewController alloc] initWithItem:item andDelegate:self];
    // Push View Controller onto Navigation Stack
    [self.navigationController pushViewController:editItemViewController animated:YES];
}

The implementation of the MTEditItemViewController class is almost complete. In its viewDidLoad method, we populate the text fields with the data of the item property. Because the setText: method of a text field only accepts a NSString instance, we need to wrap the float value of the item’s price property in a string to display it in the priceTextField text field.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Create Save Button
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(save:)];
    // Populate Text Fields
    if (self.item) {
        [self.nameTextField setText:[self.item name]];
        [self.priceTextField setText:[NSString stringWithFormat:@"%f", [self.item price]]];
    }
}

Adopting the Delegate Protocol

The final piece of the puzzle is to implement the MTEditItemViewControllerDelegate protocol. Start by importing the header file of the MTEditItemViewController class at the top of the header file and add the MTEditItemViewControllerDelegate protocol inside the angle brackets after the name of the superclass (see below).

#import <UIKit/UIKit.h>
#import "MTAddItemViewController.h"
#import "MTEditItemViewController.h"
@interface MTListViewController : UITableViewController <MTAddItemViewControllerDelegate, MTEditItemViewControllerDelegate>
@end

Adopting the MTEditItemViewControllerDelegate protocol is limited to implementing the controller:didUpdateItem: method as shown below. It might surprise you that we don’t update the data source of the table view. All that we do in the delegate method is reload one row of the table view. The reason that we don’t need to update the table view’s data source, the items array, is that the updated item was passed by reference to the edit item view controller. In other words, the object that the edit item view controller updated is the same object that is contained in the items array. This is one of the nice benefits of working with objects.

- (void)controller:(MTEditItemViewController *)controller didUpdateItem:(MTItem *)item {
    // Fetch Item
    for (int i = 0; i < [self.items count]; i++) {
        MTItem *anItem = [self.items objectAtIndex:i];
        if ([[anItem uuid] isEqualToString:[item uuid]]) {
            // Update Table View Row
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
        }
    }
    // Save Items
    [self saveItems];
}

Don’t forget to save the list of items to make sure that the edits are written to disk. Build and run the project to test the editing functionality.


Step 10:
Creating the Shopping List View Controller

Before we explore the data source of the shopping list view controller, let’s create some scaffolding to work with. Create a new UITableViewController subclass and name it MTShoppingListViewController.

Open the implementation file of the new class and add two private properties of type NSArray to the class extension, (1) items, which will contain the complete list of items and (2) shoppingList, which will only contain the items of the shopping list. Since we will be overriding the setter methods of both properties, we have to declare the properties as nonatomic. Don’t worry about the exact meaning of this for now.

#import "MTShoppingListViewController.h"
@interface MTShoppingListViewController ()
@property (nonatomic) NSArray *items;
@property (nonatomic) NSArray *shoppingList;
@end

The idea is to load the list of items every time a change is made to the list and then parse the list of items and extract only those items which have their inShoppingList property set to YES. Those items will be added to the shoppingList array. Another option would be to store the shopping list in a separate file, but the downside of this approach would be that we have to keep the items displayed in the list view controller and the items in the shopping list synchronized. This is asking for trouble if you ask me.

Custom Setters

The setter methods of items and shoppingList will do most of the heavy lifting for us. Take a look at the implementation of setItems:. Whenever the _items instance variable is updated (set with a new value), the buildShoppingList method is called on the view controller. Let’s see what the buildShoppingList method looks like.

- (void)setItems:(NSArray *)items {
    if (_items != items) {
        _items = items;
        // Build Shopping List
        [self buildShoppingList];
    }
}

The buildShoppingList method populates the shoppingList array with the items that have their inShoppingList property set to YES. In the buildShoppingList method, we create a mutable array to temporarily store the shopping list items. Next, we iterate through the list of items and add every item to the mutable array that has a inShoppingList property of YES. Finally, we set the shoppingList property with the contents of the mutable array. Don’t forget to import the header file of the MTItem class at the top.

- (void)buildShoppingList {
    NSMutableArray *buffer = [[NSMutableArray alloc] init];
    for (int i = 0; i < [self.items count]; i++) {
        MTItem *item = [self.items objectAtIndex:i];
        if ([item inShoppingList]) {
            // Add Item to Buffer
            [buffer addObject:item];
        }
    }
    // Set Shopping List
    self.shoppingList = [NSArray arrayWithArray:buffer];
}
#import "MTItem.h"

We also override the setShoppingList setter to reload the table view every time the shoppingList array is changed or updated (see below).

- (void)setShoppingList:(NSArray *)shoppingList {
    if (_shoppingList != shoppingList) {
        _shoppingList = shoppingList;
        // Reload Table View
        [self.tableView reloadData];
    }
}

Displaying the Shopping List

Implementing the methods of the UITableViewDataSource protocol should be child’s play by now. Take a look at the implementation of each method below.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.shoppingList count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell Identifier";
    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    // Fetch Item
    MTItem *item = [self.shoppingList objectAtIndex:[indexPath row]];
    // Configure Cell
    [cell.textLabel setText:[item name]];
    return cell;
}

Loading Items

As I explained earlier, the key advantage of using the list of items to store and build the shopping list is that the application stores each item in only one place. This makes updating items in the list and in the shopping list much less of a headache. The loadItems method is identical to the one we implemented in the MTListViewController class (see below).

- (void)loadItems {
    NSString *filePath = [self pathForItems];
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        // self.items = [NSMutableArray arrayWithContentsOfFile:filePath];
        self.items = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    } else {
        self.items = [NSMutableArray array];
    }
}

Of course, the pathForItems method is identical as well since we are loading the list of items from the same file as in the MTListViewController class.

- (NSString *)pathForItems {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documents = [paths lastObject];
    return [documents stringByAppendingPathComponent:@"items.plist"];
}

When you find yourself duplicating code, you should hear an alarm bell ringing. Duplicating code isn’t problematic while you are implementing a new feature. Afterwards, however, you should consider refactoring your code to minimize the amount of duplication in the application’s code base. This is a very important concept in software development and it is often referred to as DRY, Don’t Repeat Yourself. Chris Peters recently wrote an article on Nettuts+ about DRY programming.

Before we put the MTShoppingListViewController class to use, we need to update the class’ initWithStyle: method. In addition to setting the title of the view controller, we also load the list of items, which automatically populates the shoppingList array as we saw earlier.

- (id)initWithStyle:(UITableViewStyle)style {
    self = [super initWithStyle:style];
    if (self) {
        // Set Title
        self.title = @"Shopping List";
        // Load Items
        [self loadItems];
    }
    return self;
}

The final step is to initialize the shopping list view controller in the application delegate’s application:didFinishLaunchingWithOptions: method, making it the root view controller of a navigation controller, and adding that navigation controller to the tab bar controller. As always, make sure to import the header file of the MTShoppingListViewController class.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Seed Items
    [self seedItems];
    // Initialize List View Controller
    MTListViewController *listViewController = [[MTListViewController alloc] init];
    // Initialize Navigation Controller
    UINavigationController *listNavigationController = [[UINavigationController alloc] initWithRootViewController:listViewController];
    // Initialize Shopping List View Controller
    MTShoppingListViewController *shoppingListViewController = [[MTShoppingListViewController alloc] init];
    // Initialize Navigation Controller
    UINavigationController *shoppingListNavigationController = [[UINavigationController alloc] initWithRootViewController:shoppingListViewController];
    // Initialize Tab Bar Controller
    UITabBarController *tabBarController = [[UITabBarController alloc] init];
    // Configure Tab Bar Controller
    [tabBarController setViewControllers:@[listNavigationController, shoppingListNavigationController]];
    // Initialize Window
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Configure Window
    [self.window setRootViewController:tabBarController];
    [self.window setBackgroundColor:[UIColor whiteColor]];
    [self.window makeKeyAndVisible];
    return YES;
}
#import "MTShoppingListViewController.h"

Build and run the application to see if everything is working properly. Of course, the shopping list is currently empty and we don’t have a way to add items to the shopping list. Let’s remedy this in the next step.


Step 11: Adding Items to the Shopping List

As I wrote earlier, the idea is to add an item to the shopping list when it is tapped in the list view controller. To improve the user experience, if an item is present in the shopping list, we show a green checkmark to the left of the item’s name. If an item that is already in the shopping list is tapped, then it is removed from the shopping list and the green checkmark disappears. This means that we need to take a look at the tableView:didSelectRowAtIndexPath: method of the UITableViewDelegate protocol.

Before we implement tableView:didSelectRowAtIndexPath:, download the source files of this lesson. In the folder named Resources, find the files named checkmark.png and checkmark@2x.png, and add both of these files to the project. We will need them in a few moments.

In the first line of tableView:didSelectRowAtIndexPath:, we send the table view a message of deselectRowAtIndexPath:animated: to deselect the row the user tapped. Whenever a row is tapped, it should only be highlighted for an instant hence this addition. Next, we fetch the corresponding item for the selected row and update the inShoppingList property of the item (YES becomes NO and vice versa.

Based on the value of the item’s setInShoppingList property, we either show or hide the green checkmark. We show the checkmark by setting the image property of the table view cell’s imageView property. Every UITableViewCell instance contains an image view on its left (an instance of the UIImageView class). By setting the image view’s image property to nil, the image view is blank, showing no image.

The implementation of tableView:didSelectRowAtIndexPath: ends by saving the list of items to disk to make sure that the changes are permanent.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    // Fetch Item
    MTItem *item = [self.items objectAtIndex:[indexPath row]];
    // Update Item
    [item setInShoppingList:![item inShoppingList]];
    // Update Cell
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    if ([item inShoppingList]) {
        [cell.imageView setImage:[UIImage imageNamed:@"checkmark"]];
    } else {
        [cell.imageView setImage:nil];
    }
    // Save Items
    [self saveItems];
}

How does the shopping list know when an item in the list view controller is tapped by the user? The shopping list view controller will not update its table view automatically if a change was made to the list of items in the list view controller. In addition, we don’t want the list view controller and the shopping list view controller to communicate directly to one another to prevent any form of tight coupling.

One solution to this problem is the use of notifications. Whenever the list view controller makes a change to the list of items, it posts a notification with a specific name to the notification center, an object that manages notifications. Objects that are interested in certain notifications can add themselves as observers of those notifications, which means that they can respond when those notifications are posted to the notification center.

How does all this work? There are three steps, (1) the shopping list view controller starts by telling the notification center that it is interested in receiving notifications with a name of MTShoppingListDidChangeNotification, (2) the list view controller posts a notification to the notification center whenever it updates the list of items, (3) when the shopping list view controller receives the notification from the notification center, it updates its data source and table view in response.

Before we implement the three steps I just described, it is a good idea to take a closer look at the NSNotificationCenter class. In a nutshell, the NSNotificationCenter manages the broadcasting of notifications. Objects in an application can register with a notification center to receive notifications using addObserver:selector:name:object: where (1) the first argument is the object that will receive the notifications (the observer), (2) the selector is the action that will be triggered when the observer receives the notification, (3) the name is the name of the notification, and (4) the last argument is the object that sends the notification. If the last argument is set to nil, the observer receives all the notifications with the specified name.

Receiving Notifications

Revisit the initWithStyle: method of the MTShoppingListViewController class and add the view controller instance as an observer to receive notifications with a name of MTShoppingListDidChangeNotification. The action triggered when the view controller receives a notification with that name is updateShoppingList:. We set the object to nil as it doesn’t really matter which object sent the notification.

- (id)initWithStyle:(UITableViewStyle)style {
    self = [super initWithStyle:style];
    if (self) {
        // Set Title
        self.title = @"Shopping List";
        // Load Items
        [self loadItems];
        // Add Observer
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateShoppingList:) name:@"MTShoppingListDidChangeNotification" object:nil];
    }
    return self;
}

Responding to Notifications

The method that is triggered when the observer receives a notification has a specific format as you can see below. It accepts one argument, that is, the notification object which is of type NSNotification. The notification object keeps a reference to the object that posted the notification and it can also contain a dictionary with additional information. The implementation of the updateShoppingList: method is quite simple, that is, the loadItems method is called on the view controller, which means that the list of items is loaded from disk. The rest happens automatically thanks to the custom setter methods that we implemented earlier.

- (void)updateShoppingList:(NSNotification *)notification {
    // Load Items
    [self loadItems];
}

Sending Notifications

The third piece of the puzzle is posting a notification whenever the list of items is changed by the list view controller. We can do this in the saveItems method of the MTListViewController class. We first ask for a reference to the default notification center by calling defaultCenter on the NSNotificationCenter class. Next, we call postNotificationName:object: on the default notification center and pass the name of the notification, MTShoppingListDidChangeNotification, and the object posting the notification.

- (void)saveItems {
    NSString *filePath = [self pathForItems];
    [NSKeyedArchiver archiveRootObject:self.items toFile:filePath];
    // Post Notification
    [[NSNotificationCenter defaultCenter] postNotificationName:@"MTShoppingListDidChangeNotification" object:self];
}

Before you build and run the project, make sure to amend the tableView:cellForRowAtIndexPath: as shown below to display a green checkmark for items that are already present in the shopping list.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell Identifier";
    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    // Fetch Item
    MTItem *item = [self.items objectAtIndex:[indexPath row]];
    // Configure Cell
    [cell.textLabel setText:[item name]];
    [cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
    // Show/Hide Checkmark
    if ([item inShoppingList]) {
        [cell.imageView setImage:[UIImage imageNamed:@"checkmark"]];
    } else {
        [cell.imageView setImage:nil];
    }
    return cell;
}

Build and run the shopping list application to give it a spin (figure 4). Have you noticed that edits to an item are automatically reflected in the shopping list?

Building a Groceries Application From Scratch - Part 2 - Giving the Shopping List Application a Spin - Figure 4

Ready for Publication?

Great. Where is the button to publish the shopping list application to the App Store? We are not quite done yet. Even though we have laid the foundation of the shopping list application, it is not ready for publication. There are also a few things to consider.

Scalability

The shopping list application is a modest implementation of a shopping list. If the application were to contain hundreds or thousands of items then it would be pertinent to add search capabilities as well as sections to sort the items alphabetically (as we did earlier in this series). It is important to realize that the list of items is written to disk in its entirety every time an item is updated. This is no problem when the list is small, but it will be if the list grows over time to hundreds or thousands of items.

Relationships

Also, users may want to save more than one shopping list. How would you deal with that? One option would be to store each shopping list in a separate file, but how would you deal with changes made to items? Are you going to update every shopping list that contains the item? When you start dealing with relationships, it is better to choose for an SQLite data store. Core Data is an great companion if you choose to go down that road. It is a powerful framework and sports a lot of features that make a lot of the code in our shopping list application obsolete. It is true that Core Data brings a bit more overhead with it so it is key to first consider if Core Data is suitable for your application, in other words, if it is worth the overhead.

Additional Features

There is also a lot of potential for additional features. The price property of the items remains unused in the current implementation of the shopping list application. Also, it would be nice if the user could check off an item from the shopping list by tapping the item. As you can see, the current implementation of the shopping list application is only a modest start.


Conclusion

Even though the shopping list application isn’t quite ready for the App Store yet, you can’t deny that it works as we planned and it has shown you a lot of new aspects of Cocoa development, such as notifications and implementing a custom delegate protocol.

You now know what to expect from the iOS SDK and what iOS development is like. It is up to you to decide if you want to continue your journey and become a skilled iOS developer. If you do choose to continue with iOS development, then I will provide you with some great resources in next and final installment of this series!


Viewing all articles
Browse latest Browse all 1836

Trending Articles