In the next two lessons, we'll put what we learned in this series into practice by creating a shopping list application. Along the way, you'll also learn a number of new concepts and patterns, such as creating a custom model class and implementing a custom delegate pattern. We've got a lot of ground to cover so let's get started.
Outline
The shopping list application we're about to create has two features, managing a list of items and creating a shopping list by selecting items from the list.
We'll build the application with a tab bar controller to make switching between the two views fast and straightforward. In this lesson, we focus on the first feature. In the next lesson, we the finishing touches on this feature and zoom in on the shopping list, the application's second feature.
Even though the shopping list application isn't complicated from a user's perspective, there are several decisions that need to be made during its development. What type of store do we use to store the list of items? Can the user add, edit, and delete items? These are questions that we address in the next two lessons.
In this lesson, I also show you how to seed the shopping list application with dummy data to give new users something to start with. Seeding an application with data is often a good idea to help new users get up to speed quickly.
1. Creating the Project
Launch Xcode and create a new project based on the Single View Application template in the iOS > Application section.
Name the project Shopping List and enter an organization name and identifier. Set Language to Swift and Devices to iPhone. Make sure the checkboxes at the bottom are unchecked. Tell Xcode where to save the project and click Create.
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 class by selecting New > File... from the File menu. Select Cocoa Touch Class from the iOS > Source section.
Name the class ListViewController
and make it a subclass of UITableViewController
. Leave the checkbox Also create XIB file unchecked and make sure Languages is set to Swift. Tell Xcode where you want to save the class and click Create.
Open Main.storyboard, select the view controller that's already present, and delete it. Drag a UITabBarController
instance from the Object Library and delete the two view controllers that are linked to the tab bar controller. Drag a UITableViewController
from the Object Library, set its class to ListViewController
in the Identity Inspector, and create a relationship segue from the tab bar controller to the list view controller.
Select the tab bar controller, open the Attributes Inspector, and make it the storyboard's initial view controller by checking the checkbox Is Initial View Controller.
The list view controller needs to be the root view controller of a navigation controller. Select the list view controller and choose Embed In > Navigation Controller from the Editor menu.
Select the table view of the list view controller and set Prototype Cells in the Attributes Inspector to 0.
Run the application in the simulator to see if everything is set up correctly. You should see an empty table view with a navigation bar at the top and a tab bar at the bottom.
3. Creating the Item Model Class
How are we going to work with items in the shopping list application? In other words, what type of object do 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. 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're going to create a custom model class. It requires a bit more work to set up, but it'll make development much easier down the road.
Create a new class, Item
, and make it a subclass of NSObject
. Tell Xcode where to save the class and click Create.
Properties
Open Item.swift and declare four properties:
uuid
of typeString
to uniquely identify each itemname
of typeString
price
of typeFloat
inShoppingList
of typeBool
to indicate if the item is present in the shopping list
It's essential that the Item
class conforms to the NSCoding
protocol. The reason for this will become clear in a few moments. Take a look at what we've got so far. Comments are omitted.
import UIKit class Item: NSObject { var uuid: String = NSUUID().UUIDString var name: String = "" var price: Float = 0.0 var inShoppingList = false }
Each property needs to have an initial value. We set name
to an empty string, price
to 0.0
, and inShoppingList
to false
. To set the initial value of uuid
, we make use a class we haven't seen before, NSUUID
. This class helps us create a unique string or UUID. We initialize an instance of the class and ask it for the UUID
as a string by invoking UUIDString()
.
Give it a try by adding the following code snippet to the viewDidLoad()
method of the ListViewController
class.
let item = Item() print(item.uuid)
Run the application and take a look at the output in Xcode's console to see what the resulting UUID looks like. You should see something like this:
C6B81D40-0528-4D2C-BB58-6EF78D3D3DEF
Archiving
One strategy to save custom objects to disk, such as instances of the Item
class, is through a process known as archiving. We'll use NSKeyedArchiver
and NSKeyedUnarchiver
to archive and unarchive instances of the Item
class.
The class prefix, NS
, indicates that both classes are defined in the Foundation framework. 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 containing the same information.
If we want to use NSKeyedArchiver
and NSKeyedUnarchiver
to archive and unarchive instances of the Item
class, Item
needs to adopt the NSCoding
protocol. Let's start by updating the Item
class to tell the compiler Item
adopts the NSCoding
protocol.
import UIKit class Item: NSObject, NSCoding { ... }
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.
Encoding
If you create custom classes, then you are responsible for specifying how instances of that 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.
func encodeWithCoder(coder: NSCoder) { coder.encodeObject(uuid, forKey: "uuid") coder.encodeObject(name, forKey: "name") coder.encodeFloat(price, forKey: "price") coder.encodeBool(inShoppingList, forKey: "inShoppingList") }
Decoding
Whenever an encoded object needs to be converted back to an instance of the respective class, init(coder:)
is invoked. The same keys that we used in encodeWithCoder(_:)
are used in init(coder:)
. This is very important.
required init?(coder decoder: NSCoder) { super.init() if let archivedUuid = decoder.decodeObjectForKey("uuid") as? String { uuid = archivedUuid } if let archivedName = decoder.decodeObjectForKey("name") as? String { name = archivedName } price = decoder.decodeFloatForKey("price") inShoppingList = decoder.decodeBoolForKey("inShoppingList") }
Note that we use the required
keyword. Remember that we covered the required
keyword earlier in this series. Because decodeObjectForKey(_:)
returns an object of type AnyObject?
, we cast it to a String
object.
You should never directly call init(coder:)
and encodeWithCoder(_:)
. They are only called by the operating system. By conforming the Item
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 Item
class easier, we create a custom initializer that accepts a name and a price. This is optional, but it will make development easier as you'll see later in this lesson.
Open Item.swift and add the following initializer. Remember that initializers don't have the func
keyword in front of their name. We first invoke the initializer of the superclass, NSObject
. We then set the name
and price
properties of the Item
instance.
init(name: String, price: Float) { super.init() self.name = name self.price = price }
You may be wondering why we use self.name
in init(name:price:)
and name
in init(coder:)
to set the name
property. In both contexts, self
references the Item
instance we're interacting with. In Swift, you can access a property without using the self
keyword. However, in init(name:price:)
one of the parameters has a name that is identical to one of the properties of the Item
class. To avoid confusion, we use the self
keyword. In short, you can omit the self
keyword to access a property unless there's cause for confusion.
4. Loading and Saving Items
Data persistence is going to be key in our shopping list application so let's take a look at how we're going to implement data persistence. Open ListViewController.swift and declare a variable stored property, items
, of type [Item]
. Note that the initial value of items
is an empty array.
import UIKit class ListViewController: UITableViewController { var items = [Item]() ... }
The items displayed in the view controller's table view will be stored in items
. It is important that items
is a mutable array hence the var
keyword. Why? We will add the ability to add new items a bit later in this lesson.
In the initializer of the class, we load the list of items from disk and store it in the items
property that we declared a few moments ago.
// MARK: - // MARK: Initialization required init?(coder decoder: NSCoder) { super.init(coder: decoder) // Load Items loadItems() }
The view controller's loadItems()
method is nothing more than a helper method to keep the init?(coder:)
method concise and readable. Let's take a look at the implementation of loadItems()
.
Loading Items
The loadItems()
method starts with fetching the path of the file in which the list of items is stored. We do this by calling pathForItems()
, another helper method that we'll look at in a few moments. Because pathForItems()
returns an optional of type String?
, we bind the result to a constant, filePath
. We discussed optional binding earlier in this series.
// MARK: - // MARK: Helper Methods private func loadItems() { if let filePath = pathForItems() where NSFileManager.defaultManager().fileExistsAtPath(filePath) { if let archivedItems = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? [Item] { items = archivedItems } } }
What we didn't cover yet is the where
keyword in an if
statement. By using the where
keyword, we add an additional constraint to the condition of the if
statement. In loadItems()
, we make sure pathForItems()
returns a String
. Using a where
clause, we also verify that the value of filePath
corresponds to a file on disk. We use the NSFileManager
class for this.
NSFileManager
is a class we haven't worked with yet. It provides an easy-to-use API for working with the file system. We obtain a reference to an instance of the class by asking it for the default manager.
We then invoke fileExistsAtPath(_:)
on the default manager, passing in the file path we obtained in the first line of loadItems()
. If a file exists at the location specified by the file path, we load the contents of the file into the items
property. If no file exists at that location, the items
property retains its initial value, an empty array.
Loading the contents of the file is done through the NSKeyedUnarchiver
class. It can read the binary data contained in the file and convert it to an object graph, an array of Item
instances. This process will become clearer when we look at the saveItems()
method in a minute.
Let's now take a look at pathForItems()
, the helper method we invoked earlier. We first fetch the path of the Documents directory in the application's sandbox. This step should be familiar by now.
private func pathForItems() -> String? { let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true) if let documents = paths.first, let documentsURL = NSURL(string: documents) { return documentsURL.URLByAppendingPathComponent("items.plist").path } return nil }
The method returns the path to the file containing the application's list of items. We do this by fetching the path for the Documents directory of the application's sandbox and appending "items"
to it.
The beauty of using URLByAppendingPathComponent(_:)
is that the insertion of path separators is done for us wherever necessary. In other words, the system makes sure that we receive a valid file URL. Note that we invoke path()
on the resulting NSURL
instance to make sure we return a String
object.
Saving Items
Even though we won't be saving items until later in this lesson, it's a good idea to implement it while we're at it. The implementation of saveItems()
is very concise thanks to the pathForItems()
helper method.
We first fetch 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?
private func saveItems() { if let filePath = pathForItems() { NSKeyedArchiver.archiveRootObject(items, toFile: filePath) } }
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 NSKeyedArchiver
.
During this process, every object in the object graph is sent a message of encodeWithCoder(_:)
to convert it to binary data. Remember that there's rarely a need to directly call encodeWithCoder(_:)
.
To verify that loading the list of items from disk works, add a print statement to the viewDidLoad()
method of the ListViewController
class. Run the application in the simulator and check if everything's working.
override func viewDidLoad() { super.viewDidLoad() print(items) }
If you take a look at the output in Xcode's console, you'll notice that the items
property is equal to en empty array. That's what we expect at this point. What's important is that items
isn't equal to nil
. In the next step, we'll give the user a few items to work with, a process known as seeding.
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 dummy data not only helps users get 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 isn't difficult. Because we don't want to create duplicate items, we check during application launch if the data store has already been seeded with data. 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's important to think ahead. We could put the logic for seeding the data store in the ListViewController
class, but what if, in a future version of the application, other view controllers also have access to the list of items. A better place to seed the data store is in the AppDelegate
class. Let's see how this works.
Open AppDelegate.swift and amend the implementation of application(_:didFinishLaunchingWithOptions:)
to look like the one shown below.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Seed Items seedItems() return true }
The only difference with the previous implementation is that we first invoke seedItems()
. It's 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.
The implementation of seedItems()
isn't complicated. We start by storing a reference to the shared user defaults object and we then check if the user defaults database has an entry for a key with name "UserDefaultsSeedItems"
and whether this entry is a boolean with a value of true
.
// MARK: - // MARK: Helper Methods private func seedItems() { let ud = NSUserDefaults.standardUserDefaults() if !ud.boolForKey("UserDefaultsSeedItems") { if let filePath = NSBundle.mainBundle().pathForResource("seed", ofType: "plist"), let seedItems = NSArray(contentsOfFile: filePath) { // Items var items = [Item]() // Create List of Items for seedItem in seedItems { if let name = seedItem["name"] as? String, let price = seedItem["price"] as? Float { // Create Item let item = Item(name: name, price: price) // Add Item items.append(item) } } if let itemsPath = pathForItems() { // Write to File if NSKeyedArchiver.archiveRootObject(items, toFile: itemsPath) { ud.setBool(true, forKey: "UserDefaultsSeedItems") } } } } }
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 database tells us whether or not the application has already been seeded with data. This is important since we only want to seed the application once.
If the application hasn't been seeded yet, we load a property list from the application bundle, 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 Item
instances that we're about to create. For each dictionary in the seedItems
array, we create an Item
instance by invoking the initializer we declared earlier in this lesson. Each item is added 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 like we saw in the saveItems()
method of ListViewController
.
The method archiveRootObject(_:toFile:)
returns true
if the operation ended successfully and it's only then that we update the user defaults database by setting the boolean value for the key "UserDefaultsSeedItems"
to true
. The next time the application launches, the data store won't be seeded again.
You've probably noticed that we used another helper method in seedItems()
, pathForItems()
. Its implementation is identical to that of the ListViewController
class.
private func pathForItems() -> String? { let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true) if let documents = paths.first, let documentsURL = NSURL(string: documents) { return documentsURL.URLByAppendingPathComponent("items").path } return nil }
Before you run the application, make sure to copy the property list, seed.plist, to your project. It doesn't matter where you store it as long as it's included in the application's bundle.
Run the application and inspect the output in the console to see if the data store was successfully seeded with the contents of seed.plist. Note that seeding a data store with data or updating a database takes time. If the operation takes too long, the system may kill your application before it has had a chance to finish launching. Apple refers to this event as 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.
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. The implementations should look familiar if you've read the tutorial about table views.
// MARK: - // MARK: Table View Data Source Methods override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { // Dequeue Reusable Cell let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) // Fetch Item let item = items[indexPath.row] // Configure Table View Cell cell.textLabel?.text = item.name return cell }
There are two details we need to take care of before running the application, declaring the constant CellIdentifier
and telling the table view which class to use for creating table view cells.
import UIKit class ListViewController: UITableViewController { let CellIdentifier = "Cell Identifier" ... }
While you're at it, set the title
property of the list view controller to "Items"
.
override func viewDidLoad() { super.viewDidLoad() title = "Items" // Register Class tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier) }
Run the application in the simulator. This is what you should see in the simulator.
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. On iOS, a common approach to add new items to a list is by presenting the user with a modal view controller in which new data can be entered. This means that we'll need to:
- add a button to the user interface to add new items
- create a view controller that manages the view that accepts user input
- create a new item based on the user's input
- add the newly created item to the table view
Step 1: Adding a Button
Adding a button to the navigation bar requires one line of code. Revisit the viewDidLoad()
method of the ListViewController
class and update it to reflect the implementation below.
override func viewDidLoad() { super.viewDidLoad() // Register Class tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier) // Create Add Button navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "addItem:") }
In the lesson about tab bar controllers, you learned that every view controller has a tabBarItem
property. Similarly, every view controller has a navigationItem
property, a unique instance of UINavigationItem
representing the view controller in the navigation bar of the parent view controller—the navigation controller.
The navigationItem
property has a leftBarButtonItem
property, an instance of UIBarButtonItem
, that 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.
In viewDidLoad()
, we set the leftBarButtonItem
property of the view controller's navigationItem
to an instance of UIBarButtonItem
by invoking init(barButtonSystemItem:target:action:)
, passing in .Add
as the first argument. The first argument is of type UIBarButtonSystemItem
, an enumeration. The result is a system-provided instance of UIBarButtonItem
.
Even though we've already encountered the target-action pattern, the second and third parameter of init(barButtonSystemItem:target:action:)
needs an explanation. Whenever the button in the navigation bar is tapped, a message of addItem(_:)
is sent to target
, that is, self
or the ListViewController
instance.
As I said, we've already encountered the target-action pattern when we connected the touch event of a button to an action in the storyboard. This is very similar, the only difference being that the connection is made programmatically.
The target-action pattern is a common pattern in Cocoa. The idea is simple. An object keeps a reference to the message that needs to be sent and the target, an object that acts as the receiver of that message.
The message is stored as a selector. Wait a minute. 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. You can read more about selectors in Apple's Cocoa Core Competencies guide.
Before running the application in the simulator, we need to create the corresponding addItem(_:)
method in the list view controller. If we don't do this, the view controller isn't able to respond to the message it receives when the button is tapped and an exception is thrown, crashing the application.
Take a look at the format of the method definition in the next code snippet. As we saw earlier in this series, the action accepts one argument, the object that sends the message to the view controller (target). In this example, the sender is the button in the navigation bar.
func addItem(sender: UIBarButtonItem) { print("Button was tapped.") }
I've added a print statement to the method's implementation to test if everything works correctly. Build the project and run the application to test the button in the navigation bar.
Step 2: Creating a View Controller
Create a new UIViewController
subclass and name it AddItemViewController. In AddItemViewController.swift, we declare two outlets for two text fields, which we'll create in a few moments.
import UIKit class AddItemViewController: UIViewController { @IBOutlet var nameTextField: UITextField! @IBOutlet var priceTextField: UITextField! // MARK: - // MARK: View Life Cycle override func viewDidLoad() { super.viewDidLoad() } }
We also need to declare two actions in AddItemViewController.swift. The first action, cancel(_:)
, cancels the creation of a new item. The second action, save(_:)
, uses the user's input to create and save a new item.
// MARK: - // MARK: Actions @IBAction func cancel(sender: UIBarButtonItem) { } @IBAction func save(sender: UIBarButtonItem) { }
Open Main.storyboard, drag a UIViewController
instance from the Object Library to the workspace, and set its class to AddItemViewController
in the Identity Inspector.
Create a manual segue by pressing Control and dragging from the List View Controller object to the Add Item View Controller object. Select Present Modally from the menu that pops up.
Select the segue you just created, open the Attributes Inspector, and set its Identifier to AddItemViewController.
Before we add the text fields, select the add item view controller and embed it in a navigation controller by selecting Embed In > Navigation Controller from the Editor menu.
Earlier in this lesson, we programmatically added a UIBarButtonItem
to the list view controller's navigation item. Let's find out how this works in a storyboard. Zoom in on the add item view controller and add two UIBarButtonItem
instances to its navigation bar, positioning one on each side. Select the left bar button item, open the Attributes Inspector, and set Identifier to Cancel. Do the same for the right bar button item, setting its Identifier to Save.
Select the Add Item View Controller object, open the Connections Inspector on the right, and connect the cancel(_:)
action with the left bar button item and the save(_:)
action with the right bar button item.
Drag two UITextField
instances from the Object Library to the view of the add item view controller. Position the text fields as shown below. Don't forget to add the necessary constraints to the text fields.
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 Keyboard to Number Pad. This ensures that users can only enter numbers in the bottom text field. Select the Add Item View Controller object, open the Connections Inspector, and connect the nameTextField
and priceTextField
outlets with the corresponding text field in the view controller's view.
That was quite a bit of work. Everything we've done in the storyboard can also be accomplished programmatically. Some developers don't even use storyboards and create the entire application's user interface programmatically. That's exactly what happens under the hood anyway.
Step 3: Implementing addItem(_:)
With AddItemViewController
ready to use, let's revisit the addItem(_:)
action in ListViewController
. The implementation of addItem(_:)
is short as you can see below. We invoke performSegueWithIdentifier(_:sender:)
, passing in the AddItemViewController
identifier we set in the storyboard and self
, the view controller.
func addItem(sender: UIBarButtonItem) { performSegueWithIdentifier("AddItemViewController", sender: self) }
Step 4: 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 AddItemViewController
and update their implementations as shown below. We will revisit the save(_:)
action a bit later in this tutorial.
@IBAction func cancel(sender: UIBarButtonItem) { dismissViewControllerAnimated(true, completion: nil) } @IBAction func save(sender: UIBarButtonItem) { dismissViewControllerAnimated(true, completion: nil) }
When we call dismissViewControllerAnimated(_:completion:)
on the view controller whose view is presented modally, the modal view controller forwards that 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 navigation controller, which, on its turn, forwards it to the list view controller. The second argument of dismissViewControllerAnimated(_:completion:)
is a closure that's executed when the animation is completed.
Run the application in the simulator to see the AddItemViewController
class in action. When you tap the name or price text field, the keyboard should automatically pop up from the bottom.
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, because it makes our code less independent and less reusable.
The problem that we're 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 collects the information from the text fields and notifies its delegate that a new item was saved.
The delegate object should be an object conforming to a custom delegate protocol that we define. It's 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 input of the user and notifying its delegate.
Open AddItemViewController.swift and declare the AddItemViewControllerDelegate
protocol at the top. The protocol defines one method, to notify the delegate that an item was saved. It passes along the name and the price of the item.
import UIKit protocol AddItemViewControllerDelegate { func controller(controller: AddItemViewController, didSaveItemWithName name: String, andPrice price: Float) } class AddItemViewController: UIViewController { ... }
As a reminder, a protocol declaration defines or declares the methods and properties that objects conforming to the protocol should implement. Every method and property in a Swift protocol is required.
We also need to declare a property for the delegate. The delegate is of type AddItemViewControllerDelegate?
. Note the question mark, indicating it is an optional type.
class AddItemViewController: UIViewController { @IBOutlet var nameTextField: UITextField! @IBOutlet var priceTextField: UITextField! var delegate: AddItemViewControllerDelegate? ... }
As I mentioned in the lesson about table views, it's good practice to pass along 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 without the strict requirement to keep a reference to the delegate.
Notifying the Delegate
It's time to use the delegate protocol that we declared a moment ago. Revisit the save(_:)
method in the AddItemViewController
class and update its implementation as shown below.
@IBAction func save(sender: UIBarButtonItem) { if let name = nameTextField.text, let priceAsString = priceTextField.text, let price = Float(priceAsString) { // Notify Delegate delegate?.controller(self, didSaveItemWithName: name, andPrice: price) // Dismiss View Controller dismissViewControllerAnimated(true, completion: nil) } }
We use optional binding to safely extract the values of the name and price text fields. We notify the delegate by invoking the delegate method we declared earlier. At the end, we dismiss the view controller.
Two details are worth pointing out. Did you spot the question mark after the delegate property
? In Swift, this construct is known as optional chaining. Because the delegate
property is an optional, it is not guaranteed to have a value. By appending a question mark to the delegate
property when invoking the delegate method, the method is only invoked if the delegate
property has a value. Optional chaining makes your code much safer.
Also notice that we create a Float
from the value stored in priceAsString
. This is necessary, because the delegate method expects a float as its third parameter, not a string.
Responding to Save Events
The final piece of the puzzle is to make ListViewController
conform to the AddItemViewControllerDelegate
protocol. Open ListViewController.swift and update the interface declaration of ListViewController
to make the class conform to the new protocol.
import UIKit class ListViewController: UITableViewController, AddItemViewControllerDelegate { ... }
We now need to implement the methods defined in the AddItemViewControllerDelegate
protocol. In ListViewController.swift, add the following implementation of controller(_:didSaveItemWithName:andPrice:)
.
// MARK: - // MARK: Add Item View Controller Delegate Methods func controller(controller: AddItemViewController, didSaveItemWithName name: String, andPrice price: Float) { // Create Item let item = Item(name: name, price: price) // Add Item to Items items.append(item) // Add Row to Table View tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: (items.count - 1), inSection: 0)], withRowAnimation: .None) // Save Items saveItems() }
We create a new Item
instance by invoking init(name:price:)
, passing in the name and price that we receive from the add item view controller. In the next step, the items
property is updated by adding the newly created item. Of course, the table view doesn't automagically reflect the addition of a new item. We manually insert a new row into the table view. To save the changes 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 presenting it to the user. We do this in prepareForSegue(_:sender:)
as we saw earlier in this series.
// MARK: - // MARK: Navigation override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "AddItemViewController" { if let navigationController = segue.destinationViewController as? UINavigationController, let addItemViewController = navigationController.viewControllers.first as? AddItemViewController { addItemViewController.delegate = self } } }
If the segue's identifier is equal to AddItemViewController
, we ask the segue for its destinationViewController
. You may think the destination view controller is the add item view controller, but remember that the add item view controller is embedded in a navigation controller.
This means that we need to fetch the first item in the navigation controller's navigation stack, which gives us the root view controller or the add item view controller object we're looking for. We then set the delegate
property of the add item view controller to self
, the list view controller.
Run the application one more time to see how everything works together—as if by magic.
Conclusion
That was a lot to take in, but we've accomplished quite a bit already. In the next lesson, we make some changes to the list view controller to edit and remove items from the list. In that lesson, we also add the ability to create a shopping list from the list of items.
If you have any questions or comments, you can leave them in the comments below or reach out to me on Twitter.