Learn iOS 8 App Development, Second Edition (2014)
Chapter 5. Table Manners
Tables are a powerful and flexible iOS interface element. They are so flexible that—in many applications—table views are the interface. In this chapter, you’ll learn about table views and pick up some class organization and interobject communication skills in the process. By the end of this chapter, you’ll know about the following:
· Table views
· Table cells
· Cell caching
· Table editing
· Notifications
The app you’ll create in this chapter will depend a lot more on Swift code than Interface Builder. This is typical of table view interfaces because the table view classes already provide much of the look of your table, so there’s not much for you to design. (That doesn’t mean you can’t design your own, and I’ll discuss that too.) First, you need to know what a table view looks like.
Table Views
A table view is a UITableView object that presents, draws, manages, and scrolls a single vertical list of rows. Each row is one element in the table. Rows can all be alike (homogeneous) or can be substantially different (heterogeneous) from one another. A table can appear as a continuous list of rows, or it may organize rows into groups.
If you’ve used an iPhone, iPad, or iPod for more than a few minutes, you’ve seen table views in action. In fact, there are probably more than a few iOS app interfaces that you didn’t realize were table views. By the time you’re done with this section, you’ll be able to spot them from a mile away.
The overall appearance of a table is set by the table style you choose when the table view is created. Its contents can be further refined by the style and layout of the individual rows. I’ll start by describing the overall table styles.
Plain Tables
The plain table style (UITableViewStyle.Plain) is the one you’re most likely to recognize as a table view or list. The list of keyboard choices on the left of Figure 5-1 shows a plain (.Plain) table style view.
Figure 5-1. Plain table styles
In the middle of Figure 5-1 is a plain table style with an index, a common embellishment for long lists. An index adds section labels that group similar items and provides a quick way of jumping to a particular group in the list, using the index on the right.
Another, somewhat obscure, plain table style is the selection list style (shown on the right in Figure 5-1). It looks just like a plain table style with section titles but has no index. It’s used to choose one or more options from a (potentially long) list of options.
Grouped Tables
The grouped table style (UITableViewStyle.Grouped) is the other table style. This style groups sets of rows together. Each group has an optional header and footer, allowing you to surround the group with a title, description, or even explanatory text. You should immediately recognize the examples in Figure 5-2.
Figure 5-2. Grouped table style
The Settings app is built almost entirely of table views. The title above each group is a group header. The text below is a group footer. The individual setting controls are each one row of the table. It almost doesn’t look like a table at all, but it uses the same UITableView object that Figure 5-1 does. Grouped lists cannot have indexes.
The style you choose for the list sets the overall tone of your table. You then have a lot of choices when it comes to how each row looks.
Cell Styles
A table view cell object controls the appearance and content of each row. iOS comes with several styles of table cells.
· Default
· Subtitle
· Value1 (Right Detail)
· Value2 (Left Detail)
The default style (UITableViewCellStyle.Default) is the basic one, as shown in Figure 5-3.
Figure 5-3. Default cell style
The default style has a bold title. It may optionally include a small image, which appears on the left. The arrow, check mark, or control on the right is called an accessory view, and I’ll talk about them shortly.
The second major cell style is the subtitle style (UITableViewCellStyle.Subtitle), shown in Figure 5-4. Almost identical to the default style, it can include a deemphasized line of text below each title—the subtitle. The subtitle text is optional. If you leave out the subtitle, it will look like the default style.
Figure 5-4. Subtitle cell style
The last two styles are the value1 and value2 styles (UITableViewCellStyle.Value1 and UITableViewCellStyle.Value2), as shown in Figure 5-5. The value1 style, also called the right detail style (on the left in Figure 5-5), is typically used to display a series of values or settings; the title of the cell describes the value, and the field on the right shows the current value.
Figure 5-5. Value1 and value2 cell styles
The alternate .Value2 style, also called the left detail style, puts more emphasis on the value and less on its title, as shown on the right of Figure 5-5. You’ll see this style of cell used in the Contacts app. Neither the .Value1 nor .Value2 cell style allows an image.
Cell Accessories
On the right of all cell styles is the optional accessory view. iOS provides three standard accessory views, as shown in Figure 5-6.
Figure 5-6. Standard accessory views
The standard accessory views are (from left to right in Figure 5-6) the disclosure indicator, detail disclosure button, and check mark. The first two are used to indicate that tapping the row or the button will disclose—navigate to—another screen or view that displays details about that row. Nested lists are often organized this way. For example, in a table of countries, each row might navigate to another table listing the major cities in that country.
The disclosure indicator is not a control. It’s an indication that tapping anywhere in the row will navigate you to some additional information, as in the country/city example. The detail disclosure button, however, is a regular button. You must tap the accessory view button to navigate to the details. This frees the row itself to have some other purpose. The Phone app’s recent calls table works this way (see the middle of Figure 5-6); tapping a row places a call to that person, while tapping the detail disclosure button navigates to their contact information.
The check mark is just that and is used to indicate when a row has been selected or marked, for whatever purpose.
A cell’s accessory view can also be set to a control view of your choosing (such as a toggle switch). This is common in tables that display settings (see Figure 5-2).
Custom Cells
The two table view styles, four cell styles, and various accessory views provide a remarkable amount of flexibility. If you peruse the Contacts, Settings, and Music apps from Apple, it’s almost stunning the number of interfaces (dozens, by my count) that are just different combinations of the built-in table and cell styles, with judicious use of optional images, subtitles, and accessory views.
You’ll also notice cells that don’t fit any of the styles I’ve described. There’s a wildcard in the table cell deck: you can design your own cell. A UITableCell object is a subclass of UIView. So, in theory, a table cell can contain any view objects you want, even custom ones you’ve designed yourself using Swift and Interface Builder. So, don’t fret if the standard styles don’t exactly fit your needs; you can always make your own. I’ll go into the details of that in Chapter 11.
Now that you have an idea of what’s possible, it’s time to take a closer look at how tables work.
How Table Views Work
Up to this point, every visual element in your apps has been a view object. In other words, there’s been a one-to-one relationship between what you see on your device and a UIView object in your app. Table views, however, have a few issues with that arrangement, and the table view class comes with an ingenious solution.
A table view that creates a cell object for every row runs into a number of problems when the number of rows is large. It’s not hard to imagine a contact list with several hundred names or a music list with several thousand songs. If a table view had to create a cell object for every single song, it would overwhelm your app, consume a ridiculous amount of memory, require a long time to create, and generally result in a sluggish and cumbersome interface. To avoid all of these problems, table views use some clever sleight of hand.
Table Cells and Rubber Stamps
If you’ve ever filed papers with the county clerk or shopped in a supermarket in the days before UPC bar codes, you’re familiar with the idea of a rubber stamp that can be altered to stamp a particular date or price, using a dial or movable segments. It would be ludicrous if your county clerk had to have a different rubber stamp for every date. Similarly, table views don’t create cell objects for every row. They create one cell object—or at least a small number—and reuse that cell object to draw each row in the table, kind of like a rubber stamp.
Figure 5-7 shows the concept of reusing a cell object. In this figure, there are only three (principal) view objects: a UITableView object, a UITableViewCell object, and a data source object. The table view reuses the one cell object to draw each row.
Figure 5-7. Reusable cell object
The table view does this using a delegate object, just like the delegate object you used in the Shorty app. When you create a table view object, you must provide it with a data source object. Your data source object implements specific delegate functions that the table view object will send when it wants a cell object configured to draw a particular row.
Continuing with the rubber stamp analogy, pretend you have a table view that wants to print a list of products and their prices. It starts by handing your (data source) object the rubber stamp (cell object) and saying, “Please configure this stamp for the first product in the list.” Your object then sets the properties of the stamp (product name and price) and hands the configured stamp back to the table view. The table view uses the stamp to print the first row. It then turns around and repeats this process for the second row, and so on, until all of the rows have been printed.
Using this technique, a table view can draw tables that are thousands of rows tall using only a few objects. It’s fast, flexible, and wickedly efficient.
MyStuff
You’re going to create a personal inventory app named MyStuff. It’s a relatively simple app that manages a list of items you own, recording the name of each item and where you keep it (living room, kitchen, and so on).
Design
This app’s design is slightly more involved than the last two. It’s complicated, a little, by the differences between the iPhone and iPad. Apple’s Mail app looks substantially different on the iPhone versus the iPad. That’s because the iPhone is a compact interface and has only enough screen space to comfortably display one thing at a time—either the list of messages or the content of a message. The iPad is a regular-sized interface where there’s plenty of room for both. The underlying app logic is similar, but the visual design is quite different. Fortunately, theUISplitViewController class manages most of these differences for you. I’ll describe how some of it works in this chapter and go into more detail in Chapter 12. Figure 5-8 shows the compact (iPhone) design.
Figure 5-8. Sketch of MyStuff for iPhone
The compact design is simple and typical of how table views work. The main screen is a list of your items, with their description and location. Tapping an item navigates to a second screen where you can edit those values.
The regular design is less structured. In landscape orientation, the list of items will appear on the left, as shown in Figure 5-9. Tapping an item makes the details of that item appear on the right, where they can be changed. In portrait orientation (not shown), the item detail consumes the screen while the list of items becomes a pop-up that the user accesses via a button in the upper-left corner of the screen.
Figure 5-9. Sketch of MyStuff for iPad
If this interface looks familiar, it’s the same one used by Apple’s Mail app. This is not a trivial interface to program, but you’re in luck; Xcode has an app template that includes all of the code needed to make this design work. You just have to fill in the details, which is exactly what you’re going to do next.
Creating the Project
As with all apps, begin by creating a new project in Xcode. This time, choose the Master-Detail Application template, as shown in Figure 5-10.
Figure 5-10. Creating a master-detail app
The master-detail template is so named because it’s what computer developers call this kind of interface. The list is your master view, displaying a summary of all the data. The master view segues to a secondary detail view that might show more specifics about that item or provide tools for editing it.
Name the project MyStuff, choose the Swift language, and make sure Use Core Data is turned off. Set the devices option to Universal. Click Next and save your new project folder somewhere.
The first thing you’ll notice is that there’s a lot of code in this project already. The master-detail template includes all of the code needed to display a list of items, navigate to a detail interface, create new items, delete items, and handle orientation changes. The content of its table is simpleNSDate objects. Your job is to replace those placeholder objects with something of substance.
Tip You’d do well to spend some time looking over the code included by the project template. It does “all the right things” when it comes to navigation, orientation changes, and so on. You’ll read more about navigation in Chapter 12.
Creating Your Data Model
You know that you want to display a list of “things”—the individual items you own. And you know that each thing is going to need at least two properties, a name property and a location property, which are both strings. So, what object is going to represent each thing? That’s an important question because this mysterious object (or objects) is what’s called your data model. Your data model comprises the objects that represent whatever concept your table view is displaying.
Note The theory and practice behind data models are described in Chapter 8.
Clearly the Cocoa Touch framework doesn’t include such an object, so you’ll have to create one! In the library, choose the file templates tab and drag a new Swift file into your project, as shown on the left in Figure 5-11. You can also add a new file by using the New File command from the File menu or by Control+right-clicking a group in the navigator. Use whichever you find most convenient.
Figure 5-11. Creating the MyWhatsit class
Name the new file MyWhatsit, as shown on the right in Figure 5-11. Accept the default location (the MyStuff project folder), make sure the new file is a member of the MyStuff target, and click Create. Now you have a new Swift file in your app named MyWhatsit.swift. Select theMyWhatsit.swift file in the navigator. It’s pretty bleak. In fact, it doesn’t define anything at all. Get started by declaring a new MyWhatsit class of objects.
class MyWhatsit {
}
This is the class of objects that will represent each item you own. Each one will need a name property and a location property. Define those now by adding these properties to your new class:
var name: String
var location: String
Congratulations, you now have a data model. You’ll also want to create your MyWhatsit objects with something other than nothing for a name and location, so define an initializer function that creates an object and sets both properties in a single statement.
init( name: String, location: String = "" ) {
self.name = name;
self.location = location
}
This initializer function lets you create a new MyWhatsit object with a name and location (MyWhatsit("Lightsaber","guest bedroom")) or just a name (MyWhatsit("Lightsaber")). Initializers and default parameter values are described in Chapter 20.
When you’re finished, your file should look like the one in Figure 5-12.
Figure 5-12. Complete MyWhatsit.swift file
Now that you have a data model, your next task is to teach the table view class how to use it.
Creating a Data Source
A table view object (UITableView) has two delegate properties. Its delegate property works just like the delegates you used in earlier chapters. The table view delegate is optional. If you choose to use one, it must be connected to an object that adopts the UITableViewDelegateprotocol.
The table view’s other delegate is its data source object. For a table view to work, you must set its dataSource property to an object that adopts the UITableViewDataSource protocol. This delegate is not optional—without it, your table won’t display anything.
The data source’s job is to provide the table view with all the information it needs to arrange and display the contents of the table. At a minimum, your data source must do the following:
· Report the number of rows in the table
· Configure the table view cell (rubber stamp) object for each row
A data source can also provide lots of optional information to the table view. Here are the kinds of things you can customize:
· Organize rows into groups
· Display section titles
· Provide an index (for indexed lists)
· Provide custom header and footer views for grouped tables
· Control which rows are selectable
· Control which rows are editable
· Control which rows are movable
As you saw in Shorty, a single class can adopt multiple protocols and can be the delegate for more than one object. In a similar vein, your view controller object can adopt both the UITableViewDelegate and UITableViewDataSource protocols and act as both the delegate and the data source for a table view. This arrangement is so common that iOS provides a class—UITableViewController—designed just for this purpose. UITableViewController is a subclass of UIViewController that’s also a table view delegate and a table view data source. All you have to do is subclass UITableViewController and override a few functions. And you don’t even have to do the first part because the master-detail project template already did that for you.
Note UITableViewController can also control a table view that uses different objects for its delegate and/or data source. When UITableViewController loads its view, it examines the delegate and dataSource properties. If they weren’t explicitly connected in Interface Builder to some other object, it automatically makes itself the table’s delegate and data source.
Select the MasterViewController.swift file and take a look at the functions defined there. For a table view to work, your data source object must implement these two required functions:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
The first function is called on your data source object whenever the table view wants to know how many rows are in a particular section of your table. Remember, some tables can be grouped into sections, with each section having a different number of rows. For a simple table (like yours), there’s only one section (0), so just return the total number of rows.
You’re going to store your MyWhatsit objects in an array. One has already been defined in MSMasterViewController, but let’s rename it. At the top of the MasterViewController.swift file, find the objects variable. Position (hover) your cursor over the variable name for a second or two. A pop-up list control will appear just to the right of the symbol name. Gently slide over, click it, and choose the Edit All In Scope command, as shown in Figure 5-13.
Figure 5-13. Renaming a variable
Xcode selects the symbol name and highlights every other occurrence of that symbol in your file. Type the name things, and Xcode replaces all references to objects with things in a single step.
Note Xcode’s Edit All In Scope feature uses Swift’s language parser to intelligently find references to your variable. If the word objects appears in a comment or as a local variable in a loop, those instances won’t be altered.
Finally, replace the initialization of the variable with a Swift array (modified code in bold):
var things = [MyWhatsit]()
This presets the things variable to an empty array capable of storing MyWhatsit objects. Now it’s time to visit those two required data source functions.
Implementing Your Rubber Stamp
Find the tableView(_:,numberOfRowsInSection:) function. Here’s what it looks like:
override func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return things.count
}
There’s nothing to change here. The function already does exactly what you need it to do: return the number of rows (MyWhatsit objects) in your table.
Move on to the tableView(_:,cellForRowAtIndexPath:) function. The code currently looks like this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath:
NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
forIndexPath: indexPath) as UITableViewCell
let object = things[indexPath.row] as NSDate
cell.textLabel.text = object.description
return cell
}
This is your rubber stamp. This function is called every time the table view wants to draw a row. Your job is to prepare a UITableViewCell object that will draw that row and return it to the caller. This happens in two steps. The first step is to get the UITableViewCell object to use. Ignore that step for the moment; I’ll describe this process in the next section (“Table Cell Caching”).
The second step is to configure the cell so it draws the row correctly. The last three statements are where that happens. Right now, it expects to get an NSDate object from the array and set the label of the cell to its description. This is the code you need to replace. Replace the last three statements in the function with this (modified code in bold):
let thing = things[indexPath.row] as MyWhatsit
cell.textLabel?.text = thing.name
cell.detailTextLabel?.text = thing.location
return cell
Now your rubber stamp gets the MyWhatsit object for the row to be drawn (from the indexPath object) and stores it in the thing variable. It then uses the name and location properties to set the textLabel.text (title) and detailTextLabel.text (subtitle) of the cell.
Note Table views use NSIndexPath objects to identify rows in a table. The NSIndexPath objects used by UITableView have a section property and a row property that unambiguously identifies each row. Since your table has only one section, you can ignore thesection property; it will always be 0.
The cell you return will be used to draw the row. That was the easy part. Now take one step back and look at the first part of that function again.
Table Cell Caching
In the rubber stamp analogy, I said that the table view “gives you a rubber stamp and asks you to configure it.” I lied—at least a little. The table view doesn’t give you the cell object to use because it doesn’t know what kind of cell object you need. Instead, a cell object is created by either the storyboard or the code you write, and the table view hangs onto it so you can reuse it again next time. This is called the table cell cache.
There are three ways of using the table cell cache:
· Let your storyboard create the cell objects
· Lazily create cell objects programmatically, as needed
· Ignore the cache entirely
In this app, you’ll take the first approach. The master-detail project template has already defined a single table cell object, with the unimaginative identifier Cell. Select the Main.storyboard file and select the table view object in the Master Scene, as shown in Figure 5-14.
Figure 5-14. Table view with prototype table cell
At the top of the table view you’ll see a Prototype Cells region. This is where Interface Builder lets you design the rubber stamps, er, cell objects, your table view will use. The Prototype Cells count (shown in the attributes inspector on the right of Figure 5-14) declares how many different cell objects your table defines. You need only one.
Click the one and only prototype cell template, as shown in Figure 5-15. Now you’re editing a single table cell object. Notice that the Identifier property is set to Cell; this identifies the cell in the cache and must exactly match the identifier you pass in thedequeueReusableCellWithIdentifier(_:,forIndexPath:) call.
Figure 5-15. Editing a table cell prototype
Your table will display the name of the object and its location. The standard cell type that fits that description is the subtitle style (UITableViewCellStyle.Subtitle). Change the cell’s style to Subtitle, as shown in the upper right of Figure 5-15.
Your table view design is complete. You’ve defined a single cell object, it has an identifier of Cell, and it uses the subtitle table cell style.
CELL OBJECT IDENTIFIERS AND REUSE
The table view cell cache makes it easy for your tableView(_:,cellForRowAtIndexPath:) function to efficiently reuse table cell view objects, and there are a variety of different ways to use it.
The traditional way of using the table cell cache is to programmatically create your table cell view objects, as needed. This is also called lazy object creation. You do this by checking to see whether the cell object you need is already in the cache and create one only if it isn’t. The code to do that looks like this:
let cellIdent = "LazyCell"
var cell = tableView.dequeueReusableCellWithIdentifier(
cellIdent) as? UITableViewCell
if cell == nil {
cell = UITableViewCell(style: .Subtitle, reuseIdentifier: cellIdent)
cell!.accessoryType = .DisclosureIndicator
}
This code asks the table cell cache if a cell with the identifier LazyCell has already been added. If not, the message will return nil, indicating there’s no such cell in the cache. Your code responds by creating a new cell object, assigning it the same cell identifier. When you return this cell object to the table view, it will automatically add it to its cache. The next time, that cell view object will be in the cache.
A more modern approach is to register a cell view class or Interface Builder file with the table using the registerClass(_:,forCellReuseIdentifier:) or registerNib(_:,forCellReuseIdentifier:) function. After you do that, requests for a cell with that identifier that’s not in the cache will automatically create one for you using the prototype cell defined in the storyboard file. This happens automatically when you design prototype cells using the storyboard and is the technique you’re using in MyStuff.
Using cell identifiers, you can also maintain a small stable of different cell objects. In your MyStuff app, you might one day decide to have a different row design for Star Wars memorabilia and another row design for stuff you got from your grandmother. You would assign each cell object its own identifier ("Cell", "Star Wars", "Me Ma"). The table view cell cache would then keep all three cell objects, returning the appropriate one when you call any of the dequeueReusableCellWithIdentifier(_:...) functions. To do this using a storyboard, set thePrototype Cells count to 3 and assign a unique identifier to each prototype cell.
At the other extreme, you don’t have to use the cache at all. Your tableView(_:,cellForRowAtIndexPath:) function could return a new cell object every time it’s called. This would be appropriate for a tiny number of rows, where each row was completely different, or the number of rows is fixed—the kind of interface you see in the Settings app, for example.
Xcode lets you create this kind of table right in Interface Builder. Using the attributes inspector, change the table view’s Content property from Dynamic to Static. Now the cells you add to your table are exactly the cells the table will display. For this kind of table, you don’t override thetableView(_:,numberOfRowsInSection:) or tableView(_:,cellForRowAtIndexPath:) function.
You are free to mix and match any of these techniques. A single table could have some cell view objects that are defined in the storyboard and others registered to be created by class name, and your code could lazily create the rest.
While you’re here, change the name in the navigation bar from Master to My Stuff. Do this by double-clicking the Master title in navigation bar above the table view or locating the navigation item object (see Figure 5-16). You’ve now implemented all of the code needed to display yourMyWhatsit objects in a table view, but there are still a few problems. You changed the type (and name) of the original objects array, and there’s still some stray code—from the project template—using that as if it were an array of NSDate objects. Swift is very precise about the types of values and won’t compile that code.
Figure 5-16. Renaming the master view
The code that’s giving you problems is not important now; let’s just ignore it. Locate the insertNewObject(sender:) and prepareForSegue(_:,sender:) functions in the MasterViewController.swift file and “comment out” their code; select the statements inside the outer { and } braces and choose the Editor Structure
Comment Selection command, as shown in Figure 5-17.
Figure 5-17. “Commenting out” a block of code
There’s one more tiny problem. When you replaced NSMutableArray() with an [MyWhatsit]() array, you inadvertently changed the type of the array from a foundation NSArray object to a native Swift array object. These two array types are largely interchangeable, but (again) Swift is picky about how you use them. NSArray has a removeObjectAtIndex() function. Swift arrays have the same function, but it’s called removeAtIndex(). Locate the tableView(_:,commitEditingStyle:,forRowAtIndexPath:) function and change theremoveObjectAtIndex() line so it reads as follows (modified code in bold):
things.removeAtIndex(indexPath.row)
Finally, your project builds! There’s only one thing missing...
Where’s the Beef?
You can run your app right now, but it won’t display anything. That’s because you don’t have any MyWhatsit objects to display. To make things worse, you haven’t written any of the code to create or edit objects yet.
My solution in these situations is to cheat; programmatically create a few test objects so the interface has something to display. Find the var things property in MasterViewController.swift. Replace the initialization statement so it looks like this (modified code in bold):
var things: [MyWhatsit] = [
MyWhatsit(name: "Gort", location: "den"),
MyWhatsit(name: "Disappearing TARDIS mug", location: "kitchen"),
MyWhatsit(name: "Robot USB drive", location: "office"),
MyWhatsit(name: "Sad Robot USB hub", location: "office"),
MyWhatsit(name: "Solar Powered Bunny", location: "office")
]
This code preinitializes the things array with five new MyWhatsit objects. Now when your controller is first created, it will have a set of MyWhatsit objects to show.
Testing MyStuff
Set your scheme to one of the iPhone simulators and run your app. Your table view of MyWhatsit objects appear, as shown on the left in Figure 5-18.
Figure 5-18. Working table view
That’s pretty cool! You’ve created your own data model object and implemented everything required to display your custom set of objects in a table view, using a cell format of your choosing.
But it’s clear that this app isn’t finished yet. If you tap one of the rows, you get a new screen (on the right in Figure 5-18) that doesn’t have much and certainly isn’t part of your design.
While you’re at it, stop the app and change the simulator to one of the iPad devices. Run your app again. This time your interface looks substantially different, as shown in Figure 5-19.
Figure 5-19. Working table view on an iPad
The iOS classes are working behind the scenes to adapt your interface to the size class of the user’s device. On an iPad you have plenty of room for both the table view and the detail view. The split view controller steps in to show both.
The next step is to design your details view. After that, you’ll implement the code needed to edit the list, change an item, and add new ones.
Adding the Detail View
Now you’re at the second half of the master-detail design. Your detail view is controlled by the DetailViewController object. DetailViewController is a plain old UIViewController in the Detail scene. You need to create label and text field objects to display and edit your MyWhatsit properties. You’ll need to create Interface Builder outlets in DetailViewController to connect with those text fields, and you’ll need to connect them to their objects in Interface Builder. This should be familiar territory by now, so let’s get started.
Creating the Detail View
Select the Main.storyboard file and then select Detail View Controller in the Detail Scene, as shown in Figure 5-20. Select and delete the label object in the view. You don’t need it.
Figure 5-20. Template detail view
In the object library, locate the label object and add two of them to your view. Find the text field object and add two of them. Set the text of one label to Name and the other to Location. Arrange and resize them so your interface looks like the one in Figure 5-21.
Figure 5-21. Finished detail view
Choose the Editor Resolve Auto Layout Issues
Clear All Constraints in View Controller command to discard any stray constraints from the previous design. Simultaneously select all four of the view objects you just added and click the pin constraints control in the lower-right corner of the canvas, as shown in Figure 5-22. Click the three struts to add top, leading, and trailing edge constraints to all of the views, as shown on the right in Figure 5-22.
Figure 5-22. Adding detail view constraints
Switch to the DetailViewController.swift file. Change the type of the detailItem property so it’s specifically a MyWhatsit object (modified code in bold):
var detailItem: MyWhatsit? {
Delete the existing detailDescriptionLabel property and replace it with two new outlet properties.
@IBOutlet var nameField: UITextField!
@IBOutlet var locationField: UITextField!
Switch back to the Main.storyboard file. Select the Detail View Controller object and use the connections inspector to connect the two new outlets (nameField and locationField) to the appropriate text field objects in the interface, as shown in Figure 5-23.
Figure 5-23. Connecting the text field outlets
Configuring the Detail View
You might be asking how the values of a MyWhatsit object are going to get into the two UITextField objects you just created. That’s an excellent question. It’s going to happen when the user taps a row in the table view. Most of the mechanism to get from that tap to your detail view has already been written for you (as part of the master-detail project template), but it’s helpful to understand how it all works. Let’s walk through the process of tapping a row.
There are a couple of different ways to respond to the user tapping a row in a table. The “old-school” method is to override the table view delegate function tableView(_:,didSelectRowAtIndexPath:). This rather obviously named function is called when the user selects (taps) a row in your table. Your code can decide what it wants to do about that.
Using storyboards, however, you don’t have to write any code to respond to taps. The master-detail template provided a prototype cell that already had a seque—with an identifier of showDetail—attached to it. That seque presents the detail view controller when the user taps a row.
Note Attaching a seque to a prototype cell automatically sets the cell’s accessory type to .DisclosureIndicator—that right-facing arrow on the right side of the row. If you’re not using seques, remember to set the accessory type so the user knows what will happen when they tap a row.
Using the seque method, you can intercept the transition from the table view to the detail view in the prepareForSeque(_:,sender:) function. Locate that function in the MasterViewController.swift file. Hey, it looks like someone commented out all of its code! No problem. Select the commented lines and choose the Editor Structure
Comment Selection command again. This will “uncomment” the previously commented lines.
Now that you’ve fixed the type of the detailItem property in the details view controller, you can modify this code so it will compile. Edit the code so it looks like this (modified code in bold):
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow() {
let thing = things[indexPath.row]
let controller = (segue.destinationViewController as
UINavigationController).topViewController as DetailViewController
controller.detailItem = thing
controller.navigationItem.leftBarButtonItem =
self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
The code compiles successfully because the detailItem property is now a MyWhatsit object, the same type you retrieve from the things array.
The prepareForSeque(_:,sender:) function is your opportunity to do anything that needs to be done before the interface transitions to the next view controller. For this app, you catch that seque (named showDetail) and set the detailItem property of the detail view controller to the MyWhatsit object displayed in the row the user tapped. That information is obtained from the indexPathForSelectedRow() function.
The didSet observer for the detailItem property calls the configureView() function. That function’s job is to populate the text fields in the interface with the properties of the MyWhatsit object being edit. Edit the configureView() function, in theDetailViewController.swift file, so it looks like this (modified code in bold):
func configureView() {
if let detail = detailItem {
if nameField != nil {
nameField.text = detail.name
locationField.text = detail.location
}
}
}
The mystery of how you get from a tap in a table to a detail view is solved. When the user taps a row, the following steps happen:
1. The table cell triggers a seque to the details view controller.
2. prepareForSeque(_:,sender:) is called.
3. prepareForSeque gets the row index the user tapped.
4. It uses the row index to obtain the MyWhatsit object and sets the detailItem of the detail view controller.
5. The detailItem property observer calls configureView().
6. configureView() sets up the text fields from the MyWhatsit object.
This completes the detail view! Run your app and tap a row, as shown on the right in Figure 5-24.
Figure 5-24. Working detail view
You may notice that while you can edit the text fields, they don’t change anything. The last part of your app development will be to set up editing of your MyWhatsit objects—allow the user to create new ones, change them, and delete ones they don’t want.
Editing
I’m not going to lie to you; editing is hard. That’s not to say you can’t tackle it, and you’re going to add editing to MyStuff. But don’t fret, you already have a huge head start. The table view and collection classes do most of the heavy lifting, and most of the code you need to write to support table editing has already been included in your app, thanks to the master-detail project template. There’s still code you need to write, but mostly you need to understand what’s already been written and how the pieces fit together.
Editing tables can be reduced to a few basic tasks.
· Creating and inserting a new item into the table
· Removing an item from the table
· Reorganizing items in a table
· Editing the details of an individual item
Your app will allow new items to be added, existing items to be removed, and the details of an item to be edited. By default, items in a table can’t be reordered. You can enable that feature if you need to, but you won’t here.
iOS has a standard interface for deleting and reordering items in a table. You can individually delete items by swiping the row, as shown on the left in Figure 5-25, or you can tap the Edit button and enter editing mode, as shown in the middle of Figure 5-25. In editing mode, tapping the minus button next to a row will delete it. Tapping the Done button returns the table view to regular viewing. iOS also provides a standard “plus” button for you to use to trigger adding a new item, as shown on the right in Figure 5-25.
Figure 5-25. Table editing interface
These interfaces are part of the table view classes. The only work you need to do is to set up the interface objects to trigger these actions. You’ll start by providing the code to add new objects, then I’ll describe the setup that enables editing of your table, and finally you’ll write the code to edit the properties of a single MyWhatsit object.
Inserting and Removing Items
Inserting a new item into your list is a two-step process:
1. Create new objects and add them to your collection.
2. Inform that table view that you added new objects, and where.
The master-detail template includes an action function, insertNewObject(_:), that does this. The template code, however, doesn’t know about your data model, so you’ll need to make some small adjustments to create the correct kind of object.
In the MasterViewController.swift file, find the insertNewObject(_:) function. Wow, it looks like someone commented out all of its code too. Uncomment the code and edit the function so it looks like this (modified code in bold):
var itemNumber = 0
func insertNewObject(sender: AnyObject) {
let newThing = MyWhatsit(name: "My Item \(++itemNumber)")
things.insert(newThing, atIndex: 0)
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.insertRowsAtIndexPaths( [indexPath],
withRowAnimation: .Automatic)
}
Your code generates a unique name for each new item (starting with My Item 1), uses that name to create a new MyWhatsit object, and inserts that new object into the collection, at index 0.
The next step, which is important, is to tell the table view what changed in your data model—the table view isn’t psychic. Note that the first parameter to the insertRowsAtIndexPaths(_:,withRowAnimation:) call is an array of NSIndexPath objects. If you’ve added more than one item to the table, make sure each one is accounted for in the array. You’ve inserted only one here, so you need to pass only one NSIndexPath.
Tip If you want your new items to appear at the end of the list, instead of the beginning, append that new object at the end of the array (using things.append(newThing)) and then tell the table view it was added at the end (using let indexPath = NSIndexPath(forRow: things.count-1, inSection: 0)).
Run your app again and tap the + button a few times, as shown on the right in Figure 5-25. Now you may be wondering when, and how, the insertNewObject(_:) function gets called. After all, you don’t call it, and it’s not an object created in any of the Interface Builder files. The answer to that question can be found in the next section.
Enabling Table Editing
To allow any row in your table to be deleted (via the standard iOS editing features, that is), your data source object must tell the table view that it’s allowed. If you don’t, iOS won’t permit that row to be deleted. Your data source does this via its optionaltableView(_:,canEditRowAtIndexPath:) function. The master-detail template provided one for you (in MasterViewController.swift).
override func tableView(tableView: UITableView,
canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
The function provided by the template allows all rows in your table to be editable. By default, “editable” means it can be deleted. If you don’t want a row to be editable, return false.
Note Technically, the tableView(_:,canEditRowAtIndexPath:) function determines only whether a row could be edited. If it is, then the table view delegate object gets to determine how—or if—via its optionaltableView(_:,editingStyleForRowAtIndexPath:) function. The default edit style, which you’re using here, allows the row to be deleted (UITableViewCellEditingStyle.Delete).
If tableView(_:,canEditRowAtIndexPath:) returns true for a row, iOS allows the swipe gesture to delete the row. If you also want to enable “editing mode” for the entire list (where minus signs appear in each row), you hook that up in the navigation bar, provided by theUITableViewController (which your MasterViewController inherits). iOS provides all the needed button objects, and most of the behavior, that you need. All you have to do is turn them on. In your MasterViewController code, find the viewDidLoad() function. The beginning of the function should look like this:
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.leftBarButtonItem = self.editButtonItem()
let addButton = UIBarButtonItem(barButtonSystemItem: .Add,
target: self,
action: "insertNewObject:")
self.navigationItem.rightBarButtonItem = addButton
The first line calls the superclass’s viewDidLoad() function, so the superclass can do whatever it needs to do when the view objects load.
The next line creates the Edit button you see on the left side of the navigation bar (see Figure 5-24). It sets the left button to the view controller’s editButtonItem. The editButtonItem property is a preconfigured UIBarButtonItem object that’s already set up to start and stop the edit action for its table. All you need to do is get the button and add it to your interface.
The button to create and insert a new item requires a little more setup, but not much. The next line creates a new UIBarButtonItem. It will have the standard iOS + symbol (UIBarButtonSystemItem.Add). When the user taps it, it will call the insertNewObject(_:) function on this object (self). The last line adds the new toolbar button to the right side of the navigation bar.
That’s it! This is the code that adds the Edit and + buttons to your table’s navigation bar. The Edit button takes care of itself, and you configured the + button to call your controller object’s insertNewObject(_:) function when it’s tapped.
There’s one last detail you should be aware of. When adding a new object, your code created the object, added it to your data model, and then told the table view what you’d done. When deleting a row, the table view is deciding what rows to delete. So, how does the actual MyWhatsitobject get removed from the things array? That happens in this data source delegate function, which was already written for you.
override func tableView(tableView: UITableView,
commitEditingStyle editingStyle: UITableViewCellEditingStyle,
forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
things.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} ...
When a user edits a table and decides to delete (or insert) a row, that request is communicated to your data source object by calling this function. Your data source object must examine the editingStyle parameter to determine what’s happening—a row is being deleted, for example—and take appropriate action. The action to take when a row is deleted is to remove the corresponding MyWhatsit object from the array and let the table view know what you did.
That’s all of the code needed to edit your table. Now it’s time to put the last big piece of the puzzle into place: editing the details of a single item.
Editing Details
To edit the details of an item, you’re going to need to do the following:
1. Create a view where the user can see all of the details.
2. Set the values of that view with the properties of the selected item in the table.
3. Record changes to those values.
4. Update the table with the new information.
The good news is that you’ve already done half of this work. You already modified the DetailViewController to display the name and location properties of a MyWhatsit object, and you added code to fill in the text fields with the property values of the selected item (inconfigureView()). Now you just have to add some code to do the next two steps, and your app is nearly done.
Create an action that will respond to changes made to the name and location text fields. Add this function to DetailViewController.swift:
@IBAction func changedDetail(sender: AnyObject!) {
if sender === nameField {
detailItem?.name = nameField.text
} else if sender === locationField {
detailItem?.location = locationField.text
}
}
This action function will be called when either the name or location text field is edited. Since both fields call the same function, you must figure out which one sent the action by comparing the sender object with well-known view objects. If one is a match, you know which text field sent the action and can update the appropriate MyWhatsit property with the new value.
Connect the action of the two text fields to this function in Interface Builder. Select the Main.storyboard file. Select the name property text field. Control+drag from the text field to the view controller object, release the mouse button, and choose the changedDetail: action—which is currently your only action, as shown in Figure 5-26. Repeat with the location text field.
Figure 5-26. Connecting text field to changedDetail: action
Now when you edit one of the text fields in the detail view, it will change the property values of the original object, updating your data model. Give it a try.
Make sure your scheme is still set to an iPhone simulator and run your app. Your items appear in the list, shown on the left in Figure 5-27.
Figure 5-27. Testing detail editing
Tapping the Gort item shows you its details. Edit the details of the first row. In the example in Figure 5-27, I’m changing its location to “living room.” Clicking the My Stuff button in the navigation bar returns you to the list. But wait! The Gort row wasn’t updated.
Or was it? You could test this theory by setting a debugger breakpoint in the changedDetail(_:) function to see whether it was called (it was). No, the problem is a little more insidious. With your cursor (or finger, if you’re testing this on a real device), drag the list up so it causes the Gort row to disappear briefly underneath the navigation toolbar, as shown on the left in Figure 5-28.
Figure 5-28. Redrawing the first row
Release your mouse/finger and the list snaps back. Notice that the first row now shows the updated values. That’s because your changedDetail(_:) function changed the property values in your data model, but you never told the table view, so it didn’t know to redraw that row. You need to fix that.
Observing Changes to MyWhatsit
In Chapter 8 I’ll explain the rationale behind data model and view object communications. For now, all you need to know is that when the properties of a MyWhatsit object change, the table view needs to know about it so it can redraw that row.
In theory, this is an easy problem to solve: when the MyWhatsit property is updated, something needs to call a table view function to redraw the table, just like you did when you added or removed an object. In practice, it’s a little trickier. The problem is that neither the MyWhatsit object nor DetailViewController have a direct connection to the table view object of the MasterViewController view. While there’s nothing stopping you from adding one and connecting it in Interface Builder or programmatically, there’s a cleaner solution.
Note In a good model-view-controller design, it would be completely inappropriate for a data model object (such as MyWhatsit) to have a direct connection to a view object (such as a table view). So, this isn’t just a clever solution; it’s actually good software design.
There’s a software design pattern called the observer pattern. It works like this:
1. Any object interested in knowing when something happens registers as an observer.
2. When something happens, the object responsible posts a notification.
3. That notification is then distributed to all interested observers.
The real beauty of this arrangement is that neither the observers nor the objects posting notifications have to know anything about each other. You’ll use notifications to communicate changes in MyWhatsit objects to the MasterViewController. The first step is to design a notification and have MyWhatsit post it at the appropriate time.
Posting Notifications
Toward the top of your MyWhatsit.swift file, add this constant definition, before the class declaration:
let WhatsitDidChangeNotification = "MyWhatsitDidChange"
In the body of the class, add this new function:
func postDidChangeNotification() {
let center = NSNotificationCenter.defaultCenter()
center.postNotificationName(WhatsitDidChangeNotification, object: self)
}
When called, this function posts a notification named “MyWhatsitDidChange”. The object of the notification is itself. The name of the notification can be anything you want; you just want to make sure it’s unique so it isn’t confused with a notification used by another object.
Of course, you have to call this function at some point. You want to post your notification whenever anyone changes a property of your MyWhatsit object. You’ll do that by adding property observers to your two properties (new code in bold).
var name: String {
didSet {
postDidChangeNotification()
}
}
var location: String {
didSet {
postDidChangeNotification()
}
}
A property observer is a block of code executed whenever that property value is set (that is, something.name = anyValue). The didSet observer is executed after the property is set, which is a perfect place to fire your notification. (There’s also a willSet observer that executes before, should you need that.)
Note Property observers are never executed during object initialization. So, the self.name = name statement in your init(name:,location:) initializer won’t post a notification.
Now whenever you change the details of your MyWhatsit object, it will post a notification that it changed. Any object interested in that fact will receive that notification. The last step is to make MasterViewController observe this notification.
Observing Notifications
The basic pattern for observing notifications is as follows:
1. Create a function to receive the notification.
2. Become an observer for the specific notifications your object is interested in.
3. Process any notifications received.
4. Stop observing notifications when you don’t need them anymore or before your object is destroyed.
The first step is simple enough. In your MasterViewController.swift file, add a whatsitDidChange(_:) function.
func whatsitDidChange(notification: NSNotification) {
if let changedThing = notification.object as? MyWhatsit {
for (index,thing) in enumerate(things) {
if thing === changedThing {
let path = NSIndexPath(forItem: index, inSection: 0)
tableView.reloadRowsAtIndexPaths( [path],
withRowAnimation: .None)
}
}
}
}
All notification functions follow the same pattern: func myNotification(notification: NSNotification) -> Void. You can name your function whatever you want, but it must expect a single NSNotification object as its only parameter.
The notification parameter has all the details about the notification. Often you don’t care, particularly if your object wants to know only that the notification happened and not exactly why. In this case, you’re interested in the object property of the notification. Every notification has aname and an object it’s associated with—often it’s the object that caused the notification.
The first line of your function gets the notification’s object and, assuming the property is set and contains a MyWhatsit object, assigns it to the changedThing constant.
The loop then looks through your array of MyWhatsit objects. If the MyWhatsit object that changed is part of your data model, notify the table view that one of its rows needs to be updated. If the object that was modified isn’t in your data model, ignore it.
Now you just have to register MasterViewController with the notification center so it will receive this notification.
Locate the viewDidLoad() function. At the end of the function, add these statements:
let center = NSNotificationCenter.defaultCenter()
center.addObserver( self,
selector: "whatsitDidChange:",
name: WhatsitDidChangeNotification,
object: nil)
This call tells the notification center to register this object (self) and call the function (whatsitDidChange (_:)) whenever a notification with the name WhatsitDidChangeNotification is posted for any object (nil).
NOTIFICATION MATCHING
Registering to be a notification observer is very flexible. By passing nil for either the name parameter or the object parameter in addObserver(_:,selector:,name:,object:), you can request to receive notifications with a given name, for a specific object, or both. The following table shows the effect of the name and object parameters when becoming an observer:
name |
object |
Notifications received |
"Name" |
object |
Receive only notifications named Name for the object object |
"Name" |
nil |
Receive all notifications named Name for any object |
nil |
object |
Receive every notification for the object object |
nil |
nil |
Receive every notification (not recommended) |
In this situation, you want to be notified when any MyWhatsit object is edited. Your code then looks at the specific object to determine whether it’s interesting. In other situations, you’ll want to receive notifications only when a specific object sends a specific notification, ignoring similar notifications from unrelated objects.
Just as important as registering to receive notifications is to unregister when your object should no longer receive them. For this app, there’s no point at which the notifications are irrelevant, but you should still make sure that your object is no longer an observer before it’s destroyed. Leaving a destroyed object registered to receive notifications is a notorious cause of app crashes in iOS. So, make absolutely sure your object is removed from the notification center before it ceases to exist.
It’s really easy to ensure this, so you don’t have any excuses for not doing it. Add a deinitializer to your MasterViewController class.
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
The special deinitializer function is called just before the object is destroyed. In it, you should clean up any “loose ends” that wouldn’t be taken care of automatically. This statement tells the notification center that this object is no longer an observer for any notification. You don’t even have to remember what notifications or objects you’d previously ask to observe; this call will deregister them all.
Run your app in an iPhone simulator again. Edit an item and return to the list. This time your changes appear in the list!
Modal vs. Modeless Editing
You’re in the home stretch. In fact, you’re so close to the finish line that you can almost touch it. There’s only one vexing detail to fix: the iPad interface.
The iPhone interface uses what software developers call a model interface: when you tap a row to edit an item, you’re transported to a screen where you can edit its details (editing mode), and then you exit that screen and return to the list (browsing mode).
The iPad interface doesn’t work like that. Particularly in landscape orientation, you can jump between the master list and the detail view at will. This means you can start editing a title or location and then switch immediately to another item in the list. This is called a modeless interface
While this makes for a fluid user experience, it’s a disaster for your app, as shown in Figure 5-29. Tap an item (“Gort” in this example), edit one of the fields, and then switch to another item. Go ahead; give it a try. I’ll wait.
Figure 5-29. iPad table view update problem
The problem is that the editing of the text field never gets a chance to “end” before you can change to another MyWhatsit object in the list. Fortunately, there’s an easy out. When you connected your text field to the details view controller, you connected the Editing Did End event. This is the default action event for text fields. But there are many other events. Try connecting the Editing Changed event instead.
Select a text field in your details view controller, as shown on the left in Figure 5-30. Using the connections inspector, click the little (x) next to the Editing Did End event to disconnect that action. Now drag the Editing Changed event connector to the view controller, connecting it to the changedItem: action, as shown on the right in Figure 5-30. This event is a “lower-level” event that’s sent whenever the user makes any change in the text field. Now the rows in the table view will update as the user edits the details.
Figure 5-30. Reconnecting the text field action
Little Touches
Polish your app by giving it an icon, just as you did for the EightBall app in the previous chapter. Locate the Learn iOS Development Projects folder you downloaded in Chapter 1. Inside the Ch 5 folder you’ll find the MyStuff (Icons) folder. Select theimages.xcassets item in the navigator and then select the AppIcon image group. Drag all of the image files from the MyStuff (Icons) folder and drop them into the group. Xcode will sort them out.
Your app is finished, but I’d like to take a moment to direct you to other table-related topics.
Advanced Table View Topics
You can now see that table views are used for a lot more than just listing contacts and song titles. The table view classes are powerful and flexible, but that means they are—at times—complicated and confusing. The good news is that they are extensively documented, and there are lots of sample projects, which you can download from Apple, that demonstrate various table view techniques.
The place to start is the Table View Programming Guide for iOS. Choose Help Documentation and API Reference, in the search field enter Table View Programming, and click the Table View Programming Guide for iOS in the autocompletion list, as shown in Figure 5-31.
Figure 5-31. Locating the Table View Programming Guide for iOS
This guide will explain every major feature of table views and how to use them. It’s not a short read, but if you want to know how to do something specific—such as create an indexed list—this is where you should start.
Most major iOS classes have links in their documentation that will take you to a guide explaining how to use it and related classes. In the overview section for the UITableView class, for example, there are several links to table-specific programming guides.
Summary
Give yourself a big “high five!” You’ve taken another huge step in iOS app development. You’ve learned how table views works and how to use cell objects. You know what messages your app receives when a user taps a row, how to handle editing of rows, and how to create new rows. You created a data model, and you learned how to post and observe notifications between unconnected objects.
This app still falls short in a few categories. The details of a particular item could be, well, more detailed. But probably the most annoying issue is that your app doesn’t remember anything. If you restart your app, any changes you made are lost. So, for an app that’s supposed to keep track of your stuff, it doesn’t do a very good job.
Don’t worry; you’ll attack those shortcomings in future chapters. Before you get there, take a well-deserved rest from app development and take a brief stroll through the theory of object-oriented programming.