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

Building a Shopping List Application From Scratch – Part 1

$
0
0

In the next two lessons, we will put the knowledge learned in this session into practice by creating a basic shopping list application. Along the way, you will also learn a number of new concepts and patterns, such as creating a custom model class and implementing a custom delegate pattern. We have a lot of ground to cover, so let’s get started!


Outline

The shopping list application that we are about to create has two core features, (1) managing a list of items (figure 1), and (2) creating a shopping list by selecting one or more items from the list of items (figure 2). We will build the application around a tab bar controller to make switching between the two views fast and straightforward. In this lesson, we will focus on the first core feature, the list of items. In the next lesson, we will put the finishing touches on list of items and we zoom in on the shopping list, the application’s second core feature.

Building a Shopping List Application From Scratch - Part 1 - The First Tab Manages a List of Items - Figure 1
Building a Shopping List Application From Scratch - Part 1 - The Second Tab Contains the Shopping List - Figure 2

Even though the shopping list application is not complicated from a user’s perspective, there are several decisions that we need to make during its development. What type of store will we use to store the list of items? Can the user add, edit, and delete items from the list? These are questions that we will address at some point in the next two lessons.

In this lesson, I will also show you how to seed the shopping list application with some data to give users something to start with. Seeding an application with data is often a great idea to help new users get up to speed quickly.


Step 1: Creating the Project

Launch Xcode and create a new iOS project based on the Empty Application project template (figure 3). Name the project Shopping List and enter an organization name, company identifier, and class prefix (figure 4). Set Devices to iPhone and enable ARC (Automatic Reference Counting) for the project (figure 4). Tell Xcode where to save the project and hit Create.

Building a Shopping List Application From Scratch - Part 1 - Creating a New Project - Figure 3
Building a Shopping List Application From Scratch - Part 1 - Configuring the Project - Figure 4

Step 2: Creating the List View Controller

As you might have expected, the list view controller is going to be a subclass of UITableViewController. Create a new Objective-C class (File > New > File…, figure 5) with a name of MTListViewController and make it a subclass of UITableViewController (figure 6). Tell Xcode where you want to save the files of the new class and hit Create.

Building a Shopping List Application From Scratch - Part 1 - Creating a New Objective-C Class - Figure 5
Building a Shopping List Application From Scratch - Part 1 - Configuring the New Objective-C Class - Figure 6

Open the application delegate’s implementation file (MTAppDelegate.m) and create an instance of the new class. As I mentioned at the start of this tutorial, the root view controller of our application is going to be a tab bar controller, which means that we also need to create an instane of UITabBarController. To complicate matters even more, the list view controller is going to be the root view controller of a navigation controller. It will provide us with more options down the road and it will also give us a navigation bar for free, which we will need a bit later in this tutorial.

Take a look at the implementation of application:didFinishLaunchingWithOptions: to see how these three view controllers work together. Don’t forget to import the header file of the MTListViewController class. Even though the setup may look complex, there is nothing that we haven’t yet covered in this series.

#import "MTListViewController.h"
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Initialize List View Controller
    MTListViewController *listViewController = [[MTListViewController alloc] init];
    // Initialize Navigation Controller
    UINavigationController *listNavigationController = [[UINavigationController alloc] initWithRootViewController:listViewController];
    // Initialize Tab Bar Controller
    UITabBarController *tabBarController = [[UITabBarController alloc] init];
    // Configure Tab Bar Controller
    [tabBarController setViewControllers:@[listNavigationController]];
    // 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;
}

Build and run the project. You should see a tab bar with one tab at the bottom, an empty navigation bar at the top, and an empty table view in the center.

Building a Shopping List Application From Scratch - Part 1 - A Humble Beginning to the Shopping List Application - Figure 7

Step 3: Creating the Item Model Class

How are we going to work with items in the shopping application? In other words, what type of object will we use to store the properties of an item, such as its name, price, and a string that uniquely identifies each item? The most obvious choice is to store the item’s properties in a dictionary (NSDictionary). Even though this would work just fine, it would severely limit and slow us down as the application gains in complexity.

For the shopping list application, we will create a custom model class. It requires a bit more work to set up, but it will make development much easier down the road. Create a new Objective-C class with a name of MTItem and make it a subclass of NSObject. Tell Xcode where to save the class and click the Create button.

Properties

Add four properties to the header file of the new model class, (1) uuid of type NSString to uniquely identify each item, (2) name also of type NSString, (3) price of type float, and (4) inShoppingList of type BOOL to indicate whether the item is present in the shopping list. It is also key that the MTItem class conforms to the NSCoding protocol. The reason for this will become clear in a few moments. Inspect the complete header file (without comments) below:

#import <Foundation/Foundation.h>
@interface MTItem : NSObject <NSCoding>
@property NSString *uuid;
@property NSString *name;
@property float price;
@property BOOL inShoppingList;
@end

Archiving

One strategy to save custom objects such as instances of the MTItem class to disk is through a process known as archiving. We will use NSKeyedArchiver and NSKeyedUnarchiver to archive and unarchive instances of the MTItem class. Both classes are defined in the Foundation framework as their class prefix (NS) indicates. The NSKeyedArchiver class takes a set of objects and stores them to disk as binary data. An added benefit of this approach is that binary files are generally smaller than plain text files that contain the same information.

If we want to use NSKeyedArchiver and NSKeyedUnarchiver to archive and unarchive instances of the MTItem class, the latter needs to adopt the NSCoding protocol (as we specified in the header file of MTItem). Remember from the lesson about the Foundation framework, the NSCoding protocol declares two methods that a class must implement to allow instances of the class to be encoded and decoded. Let’s see how this works.

If you create custom classes, then you are responsible for specifying how instances of the class should be encoded (converted to binary data). In encodeWithCoder:, the class conforming to the NSCoding protocol specifies how instances of the class should be encoded. Take a look at the implementation below. The keys that we use are not that important, but you generally want to use the property names for clarity.

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.uuid forKey:@"uuid"];
    [coder encodeObject:self.name forKey:@"name"];
    [coder encodeFloat:self.price forKey:@"price"];
    [coder encodeBool:self.inShoppingList forKey:@"inShoppingList"];
}

Whenever an encoded object needs to be converted back to an instance of the respective class, it is sent a message of initWithCoder:. The same keys that we used in encodeWithCoder: are used in initWithCoder:. This is very important.

- (id)initWithCoder:(NSCoder *)decoder {
    self = [super init];
    if (self) {
        [self setUuid:[decoder decodeObjectForKey:@"uuid"]];
        [self setName:[decoder decodeObjectForKey:@"name"]];
        [self setPrice:[decoder decodeFloatForKey:@"price"]];
        [self setInShoppingList:[decoder decodeBoolForKey:@"inShoppingList"]];
    }
    return self;
}

You never need to call any of these methods directly. They are only called by the operating system. By conforming the MTItem class to the NSCoding protocol, we only tell the operating system how to encode and decode instances of the class.

Creating Instances

To make the creation of new instances of the MTItem class easier, we create a custom class method. This is completely optional, but it will make development easier as you will see later in this lesson. Open the header file of the MTItem class and add the following method declaration. The + sign that precedes the method declaration indicates that this is a class method as opposed to an intance method.

+ (MTItem *)createItemWithName:(NSString *)name andPrice:(float)price;

The implementation of the class contains one new element, the NSUUID class. The implementation of createItemWithName:andPrice: starts with the creation of a new instance of the class, followed by the configuration of the new instance by setting its properties. By default, a new item is not present in the shopping list so we set the inShoppingList property to NO. Setting the uuid property is done by asking the NSUUID class for an instance of the class and asking the returned instance for a uuid string. As I said, it is important that we can uniquely identify each instance of the MTItem class. The uuid will look something like 90A0CC77-35BA-4C09-AC28-D196D991B50D.

+ (MTItem *)createItemWithName:(NSString *)name andPrice:(float)price {
    // Initialize Item
    MTItem *item = [[MTItem alloc] init];
    // Configure Item
    [item setName:name];
    [item setPrice:price];
    [item setInShoppingList:NO];
    [item setUuid:[[NSUUID UUID] UUIDString]];
    return item;
}

Step 4: Load and Save Items

Data persistence is going to be key in the shopping list application, so let’s take a look at how to implement this. Open the implementation file of MTListViewController and add a private property of type NSMutableArray and name it items (see below). The items displayed in the view controller’s table view will be stored in items. It is important that items is a mutable array as we will add the ability to add new items a bit later in this lesson.

#import "MTListViewController.h"
@interface MTListViewController ()
@property NSMutableArray *items;
@end

In the class’ initialization method, we load the list of items from disk and store it in the private items property that we declared a few moments ago. We also set the view controller’s title to Items as shown below. The view controller’s loadItems method is nothing more than a helper method to keep the class’ initWithStyle: method concise and readable. Let’s take a look at the implementation of loadItems.

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

Loading Items

The loadItems method starts with retrieving the path of the file in which the list of items is stored. We do this by calling pathForItems, another helper method that we will look at in a few moments. The NSFileManager class, in the next line of code, is a class that we haven’t worked with yet. The NSFileManager class provides an easy-to-use Objective-C API for working with the file system. We obtain a reference to an instance of the class by asking it for the default manager. The default manager is sent a message of fileExistsAtPath: and passed the file path we obtained in the first line. If a file exists at the location specified by the file path, then we load the contents of the file into the items property. If no file exists at that location, then we instantiate an empty mutable array.

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

Loading the file into the view controller’s items property is done through the NSKeyedUnarchiver class as we discussed earlier. The class is able to read the binary data contained in the file and convert it to an object graph, an array of MTItem instances in this example. This process will become clearer when we look at the saveItems method in a minute.

If we were to load the contents of a file that doesn’t exist, then the items property would be set to nil instead of an empty mutable array. This is a subtle but important difference as we will see a bit later in this tutorial.

Let’s now take a look at the pathForItems helper method. The method starts with retrieving the path of the Documents directory in the application’s sandbox. This step should be familiar. The method returns the path to the file that contains the application’s list of items by appending a string to the path to the documents directory. You may want to read the previous sentence a few times to let it sink in. The beauty of using stringByAppendingPathComponent: is that the insertion of path separators is done for us wherever necessary. In other words, the system makes sure that we receive a properly formatted file path.

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

Saving Items

Even though we won’t be saving items until later in this lesson, it is a good idea to implement it while we are at it. The implementation of the saveItems method is very concise thanks to the pathForItems helper method. We first retrieve the path to the file that contains the application’s list of items and then write the contents of the items property to that location. Easy. Right?

- (void)saveItems {
    NSString *filePath = [self pathForItems];
    [NSKeyedArchiver archiveRootObject:self.items toFile:filePath];
}

As we saw earlier, the process of writing an object graph to disk is known as archiving. We use the NSKeyedArchiver class to accomplish this by calling archiveRootObject:toFile: on the NSKeyedArchiver class. In this process, every object in the object graph is sent a message of encodeWithCoder: to convert it into binary data. As I told you earlier, you usually don’t call encodeWithCoder: directly.

To verify that loading the list of items from disk works, place a log statement in the viewDidLoad method of the MTListViewController class as shown below. Build and run the project and check if everything is working.

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"Items > %@", self.items);
}

If you take a look at the output in the console window, you will notice that the items property is equal to en empty array as we expect at this point. It is important that items is not equal to nil. In the next step, we will give the user a few items to work with, a process that is known as seeding.


Step 5: Seeding the Data Store

Seeding an application with data can often mean the difference between an engaged user and a user quitting the application after using it for less than a minute. Seeding an application with some data not only helps users getting up to speed, it also shows new users how the application looks and feels with data in it.

Seeding the shopping list application with an initial list of items is not difficult. When the application launches, we first check if the data store has already been seeded with data, because we don’t want to create duplicate items. This would only confuse or frustrate the user. If the data store hasn’t been seeded yet, we load a list with seed data and use that list to create the data store of the application.

The logic for seeding the data store can be called from a number of locations in an application, but it is important to think ahead. We could put the logic for seeding the data store in the MTListViewController class, but what if, in a future version of the application, other view controllers also have access to the list of items. A good place to seed the data store is in the application delegate class. Let’s see how this works.

Open MTAppDelegate.m and amend the implementation of application:didFinishLaunchingWithOptions: to look like the one shown below. The only difference with the previous implementation is that we first call seedItems on the application delegate. It is important that seeding the data store takes place before the initialization of any of the view controllers, because the data store needs to be seeded before any of the view controllers loads the list of items.

- (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 Tab Bar Controller
    UITabBarController *tabBarController = [[UITabBarController alloc] init];
    // Configure Tab Bar Controller
    [tabBarController setViewControllers:@[listNavigationController]];
    // 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;
}

The implementation of seedItems is not complicated. We start by storing a refence to the shared user defaults instance after which we check if the user defaults system (1) has an entry for a key with name MTUserDefaultsSeedItems and (2) if this entry is a boolean with a value of YES. The key can be whatever you like as long as you are consistent in naming the keys that you use. The key in the user defaults store tells us if the application has already been seeded with data or not. This is important since we only want to seed the shopping list application once.

If the application hasn’t been seeded yet, we load a property list from the application bundle named seed.plist. This file contains an array of dictionaries with each dictionary representing an item with a name and a price. Before iterating through the seedItems array, we create a mutable array to store the MTItem instances that we are about to create. For each dictionary in the seedItems array, we create an MTItem instance, using the class method we declared earlier in this lesson, and we add the instance to the items array.

Finally, we create the path to the file in which we will store the list of items and we write the contents of the items array to disk as we saw in the saveItems method in the MTListViewController class. The method archiveRootObject:toFile: returns YES if the operation ended successfully and it is only then that we update the user defaults store by setting the boolean value for the key MTUserDefaultsSeedItems to YES. The next time the application launches, the data store will not be seeded again.

- (void)seedItems {
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    if (![ud boolForKey:@"MTUserDefaultsSeedItems"]) {
        // Load Seed Items
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"seed" ofType:@"plist"];
        NSArray *seedItems = [NSArray arrayWithContentsOfFile:filePath];
        // Items
        NSMutableArray *items = [NSMutableArray array];
        // Create List of Items
        for (int i = 0; i < [seedItems count]; i++) {
            NSDictionary *seedItem = [seedItems objectAtIndex:i];
            // Create Item
            MTItem *item = [MTItem createItemWithName:[seedItem objectForKey:@"name"] andPrice:[[seedItem objectForKey:@"price"] floatValue]];
            // Add Item to Items
            [items addObject:item];
        }
        // Items Path
        NSString *itemsPath = [[self documentsDirectory] stringByAppendingPathComponent:@"items.plist"];
        // Write to File
        if ([NSKeyedArchiver archiveRootObject:items toFile:itemsPath]) {
            [ud setBool:YES forKey:@"MTUserDefaultsSeedItems"];
        }
    }
}

You probably have noticed that we made use of another helper method to retrieve the application’s documents directory. You can find its implementation below. It is very similar to the implementation of the pathForItems method in the MTListViewController class.

- (NSString *)documentsDirectory {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    return [paths lastObject];
}

Before you build and run the project, (1) import the header file of the MTItem class and (2) make sure to copy the property list, seed.plist, to your project. It doesn’t matter where you store it as long as it is included in the application’s bundle. Build and run the project once more and inspect the output in the console to see if the data store was successfully seeded with the contents of seed.plist.

#import "MTItem.h"

Keep in mind that seeding a data store with data or updating a database takes time. If the operation takes too long, the system might kill your application before it has had the chance to finish launching. Apple calls this phenomenon the watchdog killing your application. Your application is given a limited amount of time to launch. If it fails to launch within that timeframe, the operating system kills your application. This means that you have to carefully consider when and where you perform certain operations such as seeding your application’s data store.


Step 6: Displaying the List of Items

We now have a list of items to work with. Displaying the items in the table view of the list view controller isn’t difficult. Take a look at the implementation of the three methods of the UITableViewDataSource protocol shown below. Their implementations should be familiar to you by now. Don’t forget to import the header file of the MTItem class.

#import "MTItem.h"
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.items 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.items objectAtIndex:[indexPath row]];
    // Configure Cell
    [cell.textLabel setText:[item name]];
    return cell;
}
Both the registerClass:forCellReuseIdentifier: method and the actual cell reuse identifier string are included in tableView:cellForRowAtIndexPath: above for the sake of brevity. In your own projects, you should place these two lines somewhere else, such as viewDidLoad, to prevent them from being called each time a table view cell is created.

Build and run the project once more to see the list of items being displayed in the list view controller’s table view (figure 8).

Building a Shopping List Application From Scratch - Part 1 - The Table View Populated with Seed Data - Figure 8

Step 7: Adding Items – Part 1

No matter how well we craft the list of seed items, the user will certainly want to add additional items to the list. The most common approach on iOS to add new items to a list is by presenting the user with a modal view in which new data can be entered. This means that we will need to (1) add a button to add new items, (2) create a view controller that manages the view that accepts user input, (3) create a new item based on the user’s input, and (4) add the newly created item to the table view.

Adding a Button

Adding a button to the navigation bar requires only one line of code. Revisit the viewDidLoad method of the MTListViewController class and update it to reflect the implementation below. In the lesson about tab bar controllers, we saw that every view controller has a tabBarItem property. Similarly, every view controller has a navigationItem property, a unique instance of UINavigationItem, that represents the view controller in the navigation bar of the parent view controller, that is, the navigation controller. The navigationItem property has a leftBarButtonItem property, which is an instance of UIBarButtonItem and references the bar button item displayed on the left side of the navigation bar. The navigationItem property also has a titleView and a rightBarButtonItem property.

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

In the viewDidLoad method, we set the leftBarButtonItem property of the view controller’s navigationItem to an instance of UIBarButtonItem by invoking initWithBarButtonSystemItem:target:action: and passing UIBarButtonSystemItemAdd as the first argument. This method creates a system-provided instance of UIBarButtonItem. Even though we have already encountered the target-action pattern, the second and third parameter of initWithBarButtonSystemItem:target:action: might need some explaining. Whenever the button in the navigation bar is tapped, a message of addItem: is sent to target, that is, self or the instance of MTListViewController. As I said, we have already encountered the target-action pattern when we connected a button’s touch event to an action in Interface Builder a few lessons ago. This is very similar with the difference that the connection is made programmatically.

The target-action pattern is very common in Cocoa. The idea is simple. An object keeps a reference to (1) the message or action selector that needs to be sent to a particular object and (2) the target, an object to which the message needs to be sent. What is a selector? A selector is the name or the unique identifier that is used to select a method that an object is expected to execute. A selector is of type SEL and can be created using the @selector compiler directive.

Before building and running the application in the iOS Simulator, we need to create the corresponding addItem: method in the view controller. If we don’t do this, the view controller will not be able to respond to the message it receives when the button is tapped and an exception will be thrown. Take a look at the format of the method definition in the code snippet below. As we saw earlier in this series, the action accepts one argument, the object that sends the message to the view controller (target), that is, the button in the navigation bar. You can add a log statement to the method implementation to test if the setup works correctly. Build and run the project to test the button in the navigation bar.

- (void)addItem:(id)sender {
    NSLog(@"Button was tapped.");
}

Create a View Controller

Create a new UIViewController subclass (not UITableViewController), name it MTAddItemViewController, and tell Xcode to create a nib file for the class by checking the checkbox labeled With XIB for user interface.

We first need to create two outlets in the class’s header file for the two text fields that we will create in a few moments. This process should be familiar by now. Take a look at the header file (without comments) pasted below.

#import <UIKit/UIKit.h>
@interface MTAddItemViewController : UIViewController
@property IBOutlet UITextField *nameTextField;
@property IBOutlet UITextField *priceTextField;
@end

We also need to create two actions in the class’s implementation file (MTAddItemViewController.m). The first action, cancel:, will cancel the creation of a new item, whereas the second action, save:, will save the newly created item.

- (IBAction)cancel:(id)sender {
}
- (IBAction)save:(id)sender {
}

Open the nib file (MTAddItemViewController.xib) to create the user interface. Drag a navigation bar from the Object Library to the view controller and position it at the top of the view (figure 9). As you can see, a navigation bar is just another user interface element that doesn’t necessarily have to be linked to a navigation controller.

Add two UIBarButtonItem instances to the navigation bar and position one on each side of the navigation bar. Select the left bar button item, open the Attributes Inspector, and set Identifier to Cancel (figure 9). Do the same for the right bar button item, but instead set the Identifier to Save (figure 9). Next, select the File's Owner object and open the Connections Inspector. Drag from the cancel: action to the left bar button item and from the save: action to the right bar button item.

Drag two UITextField instances from the Object Library to the view controller’s view. Position the text fields as shown in the figure below (figure 9). 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. This will ensure that users can only enter numbers in the text field. Select the File’s Owner object, open the Connections Inspector, and connect the nameTextField and priceTextField outlets to the corresponding text fields in the user interface.

Building a Shopping List Application From Scratch - Part 1 - Creating the User Interface of the MTAddItemViewController class - Figure 9

That was quite a bit of work. Remember that Interface Builder is only a tool to help you create a user interface. Every action in Interface Builder can also be performed programmatically. Some developers don’t even use nib files and create the entire application’s user interface programmatically. That is exactly what happens under the hood anyway.

Implementing addItem:

With the MTAddItemViewController class ready to use, let’s revisit the addItem: action in the MTListViewController class. Before we do, however, import the header file of the MTAddItemViewController class at the top.

#import "MTAddItemViewController.h"

The implementation of the addItem: action is short as you can see below. We create a new instance of the MTAddItemViewController class and present it modally by calling presentViewController:animated:completion: on self, the list view controller instance. When presentViewController:animated:completion: is called, the view of the addItemViewController instance will slide up from the bottom and will be presented full screen. The method accepts a completion block as its third argument. Despite their usefulness, I won’t cover blocks in this tutorial as it is a more complicated topic for beginners. Instead of passing a block as the third argument, we pass nil.

- (void)addItem:(id)sender {
    // Initialize Add Item View Controller
    MTAddItemViewController *addItemViewController = [[MTAddItemViewController alloc] initWithNibName:@"MTAddItemViewController" bundle:nil];
    // Present View Controller
    [self presentViewController:addItemViewController animated:YES completion:nil];
}

Blocks have been added to the C language by Apple. They are often compared to closures in other languages. If you want to learn more about blocks, then take a look at this tutorial by Collin Ruffenach.

Dismissing the View Controller

The user should also be able to dismiss the view controller by tapping the cancel or save button of the add item view controller. Revisit the cancel: and save: actions in the MTAddItemViewController class and update their implementations as shown below. We will revisit the save: action a bit later in this tutorial.

- (IBAction)cancel:(id)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)save:(id)sender {
    [self dismissViewControllerAnimated:YES completion:nil];
}

The dismissViewControllerAnimated:completion: is interesting. By calling this method on the view controller whose view is presented modally, it forwards the message to the view controller that presented the view controller. In our example, this means that the add item view controller forwards the message to the list view controller, because the add item view controller was presented by the list view controller when the addItem: action was fired. Build and run the project to see the MTAddItemViewController class in action (figure 10).

Building a Shopping List Application From Scratch - Part 1 - The MTAddItemViewController Class In Action - Figure 10

Step 7: Adding Items – Part 2

How will the list view controller know when a new item has been added by the add item view controller? Should we keep a reference to the list view controller that presented the add item view controller? This would introduce tight coupling, which isn’t a good idea as it would make our code much less independent and reusable. The problem that we are faced with can be solved by implementing a custom delegate protocol. Let’s see how this works.

Delegation

The idea is simple. Whenever the user taps the save button, the add item view controller will collect the information from the text fields and notify its delegate that a new item was saved. The delegate object will be an object conforming to a custom delegate protocol that we will define. It is up to the delegate object to decide what needs to be done with the information the add item view controller sends. The add item view controller is only responsible for capturing the user’s input and notifying its delegate.

Open MTAddItemViewController.h and add a forward protocol declaration at the top. A forward protocol declaration is similar to a forward class declaration, which we already encountered in this series. It is a promise to the compiler that the MTAddItemViewControllerDelegate protocol is defined and exists.

Next, add a property for the delegate to the class interface. The delegate is of type id, which means that it can be any Objective-C object. However, the delegate is required to conform to the MTAddItemViewControllerDelegate protocol as specified by the name of the protocol between angle brackets after the property type. This is the reason that we added the forward protocol declaration at the top. The weak specifier in the property declaration is for memory management purposes. It indicates that the reference that the add item view controller stores to its delegate is a weak reference as opposed to a strong reference. Even though memory management is an important aspect of Cocoa development, I won’t cover weak and strong references in this lesson.

#import <UIKit/UIKit.h>
@protocol MTAddItemViewControllerDelegate;
@interface MTAddItemViewController : UIViewController
@property (weak) id<MTAddItemViewControllerDelegate> delegate;
@property IBOutlet UITextField *nameTextField;
@property IBOutlet UITextField *priceTextField;
@end

Below the class interface of MTAddItemViewController, we declare the MTAddItemViewControllerDelegate protocol. In contrast to the UITableViewDataSource and UITableViewDelegate protocols, the MTAddItemViewControllerDelegate is short and simple. The protocol declaration starts with @protocol and ends with @end, just like a class interface starts with @interface and ends with @end.

As a reminder, a protocol declaration defines or declares the methods that objects conforming to the protocol should implement. Methods in a protocol declaration are required by default. To declare optional methods, the @optional compiler directive should be used.

By adding the NSObject protocol (between angle brackets) after the name of the protocol, we declare that the MTAddItemViewControllerDelegate protocol extends (or builds upon) the NSObject protocol.

The MTAddItemViewControllerDelegate protocol defines only one method. The method informs the delegate object when a new item has been added by the user and passes the name and price of the new item.

@protocol MTAddItemViewControllerDelegate <NSObject>
- (void)controller:(MTAddItemViewController *)controller didSaveItemWithName:(NSString *)name andPrice:(float)price;
@end

As I mentioned in the lesson about table views, it is good practice to pass the sender of the message, the object notifying the delegate object, as the first argument of each delegate method. This makes it easy for the delegate object to communicate with the sender if necessary.

Notifying the Delegate

It is time to use the delegate protocol that we just declared. Revisit the save: method in the MTAddItemViewController class and update it as shown below. The contents of the text fields are first stored in the name and price variables, which are then passed as arguments to the controller:didSaveItemWithName:andPrice: delegate method.

- (IBAction)save:(id)sender {
    // Extract User Input
    NSString *name = [self.nameTextField text];
    float price = [[self.priceTextField text] floatValue];
    // Notify Delegate
    [self.delegate controller:self didSaveItemWithName:name andPrice:price];
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

Responding to Save Events

The final piece of the puzzle is to make the MTListViewController class conform to the MTAddItemViewControllerDelegate protocol. Open MTListViewController.h, import the header file of the MTAddItemViewController class, and update the interface declaration of MTListViewController to make the class conform to the new protocol.

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

The import statement is required to tell the compiler about the MTAddItemViewControllerDelegate protocol that is declared in the header file of the MTAddItemViewController class. Because we already import the header file of MTAddItemViewController in MTListViewController.h, you can remove the same import statement from the implementation file, MTListViewController.m.

As we did with the UITableViewDataSource and UITableViewDelegate protocols, we need to implement the methods defined in the MTAddItemViewControllerDelegate protocol. In MTListViewController.m, add the following implementation of controller:didSaveItemWithName:andPrice:.

- (void)controller:(MTAddItemViewController *)controller didSaveItemWithName:(NSString *)name andPrice:(float)price {
    // Create Item
    MTItem *item = [MTItem createItemWithName:name andPrice:price];
    // Add Item to Data Source
    [self.items addObject:item];
    // Add Row to Table View
    NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:([self.items count] - 1) inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationNone];
    // Save Items
    [self saveItems];
}

We create a new instance of the MTItem class, by invoking the class method we created earlier, and pass the name and price that we received from the add item view controller instance. In the next step, the items property is updated by adding the newly created item. Of course, the table view doesn’t automagically reflect that we added a new item to the list of items. We need to manually insert a new row at the bottom of the table view. To save the new item to disk, we call saveItems on the view controller, which we implemented earlier in this tutorial.

Setting the Delegate

The final piece of this somewhat complex puzzle is to set the delegate of the add item view controller when we present it to the user. We do this in the addItem: action as shown below.

- (void)addItem:(id)sender {
    // Initialize Add Item View Controller
    MTAddItemViewController *addItemViewController = [[MTAddItemViewController alloc] initWithNibName:@"MTAddItemViewController" bundle:nil];
    // Set Delegate
    [addItemViewController setDelegate:self];
    // Present View Controller
    [self presentViewController:addItemViewController animated:YES completion:nil];
}

Build and run the project one more time and see how everything works together – as by magic.


Conclusion

That was a lot to take in, but we have accomplished quite a bit already. In the next lesson, we will make some changes to the list view controller to edit and remove items from the list, and we will also add the ability to create a shopping list from the list of items.


Viewing all articles
Browse latest Browse all 1836

Trending Articles