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.
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.
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.
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.
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).
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.
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).
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.