10.
The Data Model
Written by Eli Ganim
In the previous chapter, you created a table view for Checklists, got it to display rows of items, and added the ability to mark items as completed (or not completed). However, this was all done using hardcoded, fake data. This would not do for a real to-do app since your users want to store their own custom to-do items.
In order to store, manage, and display to-do information efficiently, you need a data model that allows you to store (and access) to-do information easily. And that’s what you’re going to do in this chapter.
This chapter covers the following:
- Model-View-Controller: A quick explanation of the MVC fundamentals that are central to iOS programming.
- The data model: Creating a data model to hold the data for Checklists.
- Clean up the code: Simplify your code so that it is easier to understand and maintain.
Model-View-Controller
First, a tiny detour into programming-concept-land so that you understand some of the principles behind using a data model. No book on programming for iOS can escape an explanation of Model-View-Controller, or MVC for short.
MVC is one of the three fundamental design patterns of iOS. You’ve already seen the other two: delegation, making one object do something on behalf of another; and target-action, connecting events such as button taps to action methods.
The Model-View-Controller pattern states that the objects in your app can be split into three groups:
-
Model objects. These objects contain your data and any operations on the data. For example, if you were writing a cookbook app, the model would consist of the recipes. In a game, it would be the design of the levels, the player score, and the positions of the monsters.
The operations that the data model objects perform are sometimes called the business rules or the domain logic. For Checklists, the checklists and their to-do items form the data model.
-
View objects. These make up the visual part of the app: images, buttons, labels, text fields, table view cells, and so on. In a game, the views form the visual representation of the game world, such as the monster animations and a frag counter.
A view can draw itself and responds to user input, but it typically does not handle any application logic. Many views, such as
UITableView, can be re-used in many different apps because they are not tied to a specific data model. -
Controller objects. The controller is the object that connects your data model objects to the views. It listens to taps on the views, makes the data model objects do some calculations in response, and updates the views to reflect the new state of your model. The controller is in charge. On iOS, the controller is called the “view controller.”
Conceptually, this is how these three building blocks fit together:
The view controller has one main view, accessible through its view property, that contains a bunch of subviews. It is not uncommon for a screen to have dozens of views all at once. The top-level view usually fills the whole screen. You design the layout of the view controller’s screen in the storyboard.
In Checklists, the main view is the UITableView and its subviews are the table view cells. Each cell also has several subviews of its own, namely the text label and the accessory.
Generally, a view controller handles one screen of the app. If your app has more than one screen, each of these is handled by its own view controller and has its own views. Your app flows from one view controller to another.
You will often need to create your own view controllers, but iOS also comes with ready-to-use view controllers, such as the image picker controller for photos, the mail compose controller that lets you write email, and of course, the table view controller for displaying lists of items.
Views vs. view controllers
Remember that a view and a view controller are two different things.
A view is an object that draws something on the screen, such as a button or a label. The view is what you see. The view controller is what does the work behind the scenes. It is the bridge that sits between your data model and the views.
A lot of beginners give their view controllers names such as
FirstVieworMainView. That is very confusing! If something is a view controller, its name should end with “ViewController”, not “View”.
The data model
So far, you’ve put a bunch of fake data into the table view. The data consists of a text string and a checkmark that can be on or off. As you saw in the previous chapter, you cannot use the cells to remember the data as cells get re-used all the time and their old contents get overwritten.
Table view cells are part of the view. Their purpose is to display the app’s data, but that data actually comes from somewhere else: the data model. Remember this well: the rows are the data, the cells are the views.
The table view controller is the thing that ties them together through the act of implementing the table view’s data source and delegate methods.
The data model for this app will be a list of to-do items. Each of these items will get its own row in the table.
For each to-do item you need to store two pieces of information: the text (“Walk the dog,” “Brush my teeth,” “Eat ice cream”) and whether the checkmark is set or not.
That is two pieces of information per row, so you need two variables for each row.
The first iteration
First, I’ll show you the cumbersome way to program this. It will work but it isn’t very smart. Even though this is not the best approach, I’d still like you to follow along and copy-paste the code into Xcode and run the app so that you understand how this approach works.
Understanding why this approach is problematic will help you appreciate the proper solution better.
➤ In ChecklistViewController.swift, add the following constants right after the class ChecklistViewController line.
class ChecklistViewController: UITableViewController {
let row0text = "Walk the dog"
let row1text = "Brush teeth"
let row2text = "Learn iOS development"
let row3text = "Soccer practice"
let row4text = "Eat ice cream"
. . .
These constants are defined outside of any method (they are not “local”), so they can be used by all of the methods in ChecklistViewController.
➤ Change the data source methods to:
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return 5
}
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "ChecklistItem",
for: indexPath)
let label = cell.viewWithTag(1000) as! UILabel
if indexPath.row == 0 {
label.text = row0text
} else if indexPath.row == 1 {
label.text = row1text
} else if indexPath.row == 2 {
label.text = row2text
} else if indexPath.row == 3 {
label.text = row3text
} else if indexPath.row == 4 {
label.text = row4text
}
return cell
}
➤ Run the app. It still shows the same five rows as originally.
What have you done here? For every row, you have added a constant with the text for that row. Together, those five constants are your data model. (You could have used variables instead of constants, but since the values won’t change for this particular example, it’s better to use constants.)
In tableView(_:cellForRowAt:) you look at indexPath.row to figure out which row to display, and put the text from the corresponding constant into the cell.
Handle checkmarks
Now, let’s fix the checkmark toggling logic. You no longer want to toggle the checkmark on the cell but at the row (or data) level. To do this, you add five new instance variables to keep track of the “checked” state of each of the rows. (This time the values have to be variables instead of constants since you will be changing the checked/unchecked state for each row.) These new variables are also part of your data model.
➤ Add the following instance variables:
var row0checked = false
var row1checked = false
var row2checked = false
var row3checked = false
var row4checked = false
These variables have the data type Bool. You’ve seen the data types Int (whole numbers), Float (decimal/fractional numbers), and String (text) before. A Bool variable can hold only two possible values: true or false.
Bool is short for “boolean,” after Englishman George Boole who long ago invented a kind of logic that forms the basis of all modern computing. The fact that computers talk in ones and zeros is largely due to him.
You use Bool variables to remember whether something is true (1) or not (0). As a convention, the names of boolean variables often start with the verb “is” or “has,” as in isHungry or hasIceCream.
The instance variable row0checked is true if the first row has its checkmark set and false if it doesn’t. Likewise, row1checked reflects whether the second row has a checkmark or not, and so on.
Note: How does the compiler know that the type of these variables is
Bool? You never specified that anywhere.Remember type inference from your code in Bulls’s Eye? Because you said
var row0checked = false, the compiler infers that you intended to make this aBool, asfalseis valid only forBoolvalues.
The delegate method that handles taps on table cells will now use these new instance variables to determine whether the checkmark for a row needs to be toggled on or off.
The code in tableView(_:didSelectRowAt:) should be something like the following. Don’t make these changes just yet! Just try to understand what happens first.
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
if indexPath.row == 0 {
row0checked = !row0checked
if row0checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
} else if indexPath.row == 1 {
row1checked = !row1checked
if row1checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
} else if indexPath.row == 2 {
row2checked = !row2checked
if row2checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
} else if indexPath.row == 3 {
row3checked = !row3checked
if row2checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
} else if indexPath.row == 4 {
row4checked = !row4checked
if row4checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
}
tableView.deselectRow(at: indexPath, animated: true)
}
It should be clear that the code looks at indexPath.row to find the row that was tapped, and then performs some logic with the corresponding “row checked” instance variable. But there’s also some new stuff you may not have seen before.
Let’s look at the first if indexPath.row statement in detail:
if indexPath.row == 0 {
row0checked = !row0checked
if row0checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
} . . .
If indexPath.row is 0, the user tapped on the very first row and the corresponding instance variable is row0checked.
You do the following to flip that boolean value around:
row0checked = !row0checked
The ! symbol is the logical not operator. There are a few other logical operators that work on Bool values, such as and and or, which you’ll encounter soon enough.
What ! does is simple: it reverses the meaning of the value. If row0checked is true, then ! makes it false. Conversely, !false is true.
Think of ! as “not”: not yes is no and not no is yes. Yes?
Once you have the new value of row0checked, you can use it to show or hide the checkmark:
if row0checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
The same logic is used for the other four rows.
In fact, the other rows use the exact same logic. The only thing that is different between each of these code blocks is the name of the “row checked” instance variable.
Because the code looks so familiar from one if statement to the next, we can improve upon it.
➤ Replace the current tableView(_:didSelectRowAt:) implementation with the following:
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
var isChecked = false
if indexPath.row == 0 {
row0checked = !row0checked
isChecked = row0checked
} else if indexPath.row == 1 {
row1checked = !row1checked
isChecked = row1checked
} else if indexPath.row == 2 {
row2checked = !row2checked
isChecked = row2checked
} else if indexPath.row == 3 {
row3checked = !row3checked
isChecked = row3checked
} else if indexPath.row == 4 {
row4checked = !row4checked
isChecked = row4checked
}
if isChecked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
tableView.deselectRow(at: indexPath, animated: true)
}
Now isn’t that a lot shorter than the previous iteration (that you weren’t supposed to type in)?
Notice how the logic that sets the checkmark on the cell has moved to the bottom of the method. There is now only one place where this happens.
To make this possible, you store the value of the “row checked” instance variable into the isChecked local variable. This temporary variable is just used to remember whether the selected row needs a checkmark or not.
By using a local variable you were able to remove a lot of duplicated code, which is a good thing. You’ve taken the logic that all rows had in common and moved it out of the if statements into a single place.
Note: Code duplication makes programs a lot harder to read. Worse, it invites subtle mistakes that cause hard-to-find bugs. Always be on the lookout for opportunities to remove duplicate code!
Exercise: There was actually a bug in the previous, longer version of this method – did you spot it? That’s what happens sometimes when you use copy-paste to create duplicate code.
➤ Run the app and observe… that it still doesn’t work very well. Initially, you have to tap a couple of times on a row to actually make the checkmark go away.
What’s wrong here? Simple: when you declared the rowXchecked variables you set their values to false.
So row0checked and the others indicate that there is no checkmark on their row, but the table draws one anyway. That’s because you enabled the checkmark accessory on the prototype cell.
In other words: the data model (the “row checked” variables) and the views (the checkmarks inside the cells) are out-of-sync.
There are a few ways you could try to fix this: you could set the Bool variables to true to begin with, or you could remove the checkmark from the prototype cell in the storyboard.
Neither is a foolproof solution. What goes wrong here isn’t so much that you initialized the “row checked” values wrong or designed the prototype cell wrong, but that you didn’t set the cell’s accessoryType property to the right value in tableView(_:cellForRowAt:).
When you are asked for a new cell, you always should configure all of its properties. The call to tableView.dequeueReusableCell(withIdentifier:) could return a cell that was previously used for a row with a checkmark. If the new row shouldn’t have a checkmark, then you have to remove it from the cell at this point (and vice versa).
Let’s fix that.
➤ Add the following method to ChecklistViewController.swift. (If you’re wondering where to add the code, probably best to add it either before or after the marked sections for the table view delegates. Not that the position matters, but purely from an organizational perspective.):
func configureCheckmark(for cell: UITableViewCell,
at indexPath: IndexPath) {
var isChecked = false
if indexPath.row == 0 {
isChecked = row0checked
} else if indexPath.row == 1 {
isChecked = row1checked
} else if indexPath.row == 2 {
isChecked = row2checked
} else if indexPath.row == 3 {
isChecked = row3checked
} else if indexPath.row == 4 {
isChecked = row4checked
}
if isChecked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
This new method looks at the cell for a certain row, specified as usual by indexPath, and makes the checkmark visible if the corresponding “row checked” variable is true, or hides the checkmark if the variable is false.
This logic should look very familiar! The only difference with before is that here you don’t toggle the state of the “row checked” variable. You only read it and then set the cell’s accessory.
You’ll call this method from tableView(_:cellForRowAt:), just before you return the cell.
➤ Change tableView(_:cellForRowAt:) to the following (recall that . . . means that the existing code at that spot doesn’t change):
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
. . .
configureCheckmark(for: cell, at: indexPath)
return cell
}
➤ Run the app again.
Now the app works just fine. Initially all the rows are unchecked. Tapping a row checks it, tapping it again unchecks it. The rows and cells are now always in sync. This code guarantees that each cell always has the value that corresponds to its underlying data row.
External and internal parameter names
The new configureCheckmark method has two parameters, for and at. Its full name is therefore configureCheckmark(for:at:).
for and at are the external names of these parameters.
Adding short prepositions such as “at,” “with,” or “for” is very common in Swift. It makes the name of the method sound like a proper English phrase: “configure checkmark for this cell at that index-path.” Doesn’t it just roll off your tongue?
When you call the method, you always have to include those external parameter names:
configureCheckmark(for: someCell, at: someIndexPath)
Here, someCell is a variable that refers to a UITableViewCell object. Likewise, someIndexPath is a variable of type IndexPath.
You can’t write the following:
configureCheckmark(someCell, someIndexPath)
This won’t compile. The app doesn’t have a configureCheckmark method that doesn’t take parameter names, only configureCheckmark(for:at:). The for and at are an integral part of the method name!
Inside the method you use the internal labels cell and indexPath to refer to the parameters.
func configureCheckmark(for cell: UITableViewCell,
at indexPath: IndexPath) {
if indexPath.row == 0 {
. . .
}
cell.accessoryType = .checkmark
. . .
}
You can’t write if at.row == 0 or for.accessoryType = .checkmark. That also sounds a little odd, doesn’t it?
This split between external and internal labels is unique to Swift and Objective-C and takes some getting used to if you’re familiar with other languages.
This naming convention primarily exists so that Swift can talk to older Objective-C code, and this is a good thing since most of the iOS frameworks are still written in Objective-C.
Simplifying the code
Why was configureCheckmark(for:at:) set up as a method of its own anyway? Well, because you can use it to simplify tableView(_:didSelectRowAt:).
Notice how similar these two methods currently are. That’s another case of code duplication that you can get rid of!
You can simplify didSelectRowAt by letting configureCheckmark(for:at:) do some of the work.
➤ Replace tableView(_:didSelectRowAt:) with the following:
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
if indexPath.row == 0 {
row0checked = !row0checked
} else if indexPath.row == 1 {
row1checked = !row1checked
} else if indexPath.row == 2 {
row2checked = !row2checked
} else if indexPath.row == 3 {
row3checked = !row3checked
} else if indexPath.row == 4 {
row4checked = !row4checked
}
configureCheckmark(for: cell, at: indexPath)
}
tableView.deselectRow(at: indexPath, animated: true)
}
This method no longer sets or clears the checkmark from the cell, but only toggles the “checked” state in the data model and then calls configureCheckmark(for:at:) to update the view.
➤ Run the app again and it should still work.
➤ Change the declarations of the instance variables to the following and run the app again:
var row0checked = false
var row1checked = true
var row2checked = true
var row3checked = false
var row4checked = true
Now rows 1, 2 and 4 (the second, third and fifth rows) initially have a checkmark while the others don’t.
The approach that we’ve taken here to remember which rows are checked or not works just fine… when there’s five rows of data.
But what if you have 100 rows and they all need to be unique? Should you add another 95 “row text” and “row checked” variables to the view controller, as well as that many additional if statements? Of course not!
There is a better way: arrays.
Arrays
An array is an ordered list of objects. If you think of a variable as a container of one value (or one object) then an array is a container for multiple objects.
Of course, the array itself is also an object (named Array) that you can put into a variable. And because arrays are objects, arrays can contain other arrays.
The objects inside an array are indexed by numbers, starting at 0 as usual. To ask the array for the first object, you write array[0]. The second object is at array[1], and so on.
The array is ordered, meaning that the order of the objects it contains matters. The object at index 0 always comes before the object at index 1.
Note: An array is a collection object. There are several other collection objects and they all organize their objects in a different fashion.
Dictionary, for example, contains key-value pairs, just like a real dictionary contains a list of words and a description for each of those words. You’ll use some of these other collection types in later chapters.
The organization of an array is very similar to the rows for a table – they are both lists of objects in a particular order – so it makes sense to put your data model’s rows into an array.
Arrays store one object per index, but your rows currently consist of two separate pieces of data: the text and the checked state. It would be easier if you made a single object for each row, because then the row number from the table simply becomes the index in the array.
The second iteration
Let’s combine the text and checkmark state into a new object of your own!
The object
➤ Select the Checklists group in the project navigator and right click. Choose New File… from the pop-up menu:
Under the Source section choose Swift File:
Click Next to continue. Save the new file as ChecklistItem (you don’t really need to add the .swift file extension since it will be automatically added for you).
Press Create to add the new file to the project.
➤ Add the following to the new ChecklistItem.swift file, below the import line:
class ChecklistItem {
var text = ""
var checked = false
}
What you see here is the absolute minimum amount of code you need in order to make a new object. The class keyword names the object and the two lines with var add data items (instance variables) to it.
The text property will store the description of the checklist item (the text that will appear in the table view cell’s label) and the checked property determines whether the cell gets a checkmark or not.
Note: You may be wondering what the difference is between the terms property and instance variable – we’ve used both to refer to an object’s data items. You’ll be glad to hear that these two terms are interchangeable.
In Swift terminology, a property is a variable or constant that is used in the context of an object. That’s exactly what an instance variable is.
(In Objective-C, properties and instance variables are closely related but not quite the same thing. In Swift they are the same.)
That’s all for ChecklistItem.swift for now. The ChecklistItem object currently only serves to combine the text and the checked variables into one object. Later you’ll do more with it.
Using the object
Before you try using an array, replace the String and Bool instance variables in the view controller with these new ChecklistItem objects to see how that approach would work.
➤ In ChecklistViewController.swift, remove the old properties (both the let and var values) and replace them with ChecklistItem objects:
class ChecklistViewController: UITableViewController {
var row0item = ChecklistItem()
var row1item = ChecklistItem()
var row2item = ChecklistItem()
var row3item = ChecklistItem()
var row4item = ChecklistItem()
These replace the row0text, row0checked, etc. instance variables.
Wait a minute though… We’ve had variable declarations with a type, or with explicity values like an empty string or a number, but what are these? These variables are being assigned with what looks like a method!
And you are right about the method — it’s a special method that all classes have called an initializer method. An initializer method creates a new instance of the given object, in this case ChecklistItem .
This creates an empty instance of ChecklistItem with the the default values you defined when you added the class implementation — an empty string (””) for text and false for checked.
Instead of the above, you could have used what’s known as a type annotation to simply indicate the type of row0Item like this:
var row0item: ChecklistItem
If you did that, row0item won’t have a value yet, it would just be an empty container for a ChecklistItem object. And you’d still have to create the ChecklistItem instance later in your code (for example, in viewDidLoad).
The way we’ve done the code now, we initialize the variables above immediately with an empty instance of ChecklistItem and let Swift’s type inference do the work in letting the compiler figure out the type of the variables. Handy, right?
Just to clarify the above a bit more, the data type is like the brand name of a car. Just saying the words “Porsche 911” out loud doesn’t magically get you a new car – you actually have to go to the dealer to buy one.
The parentheses () behind the type name are like going to the object dealership to buy an object of that type. The parentheses tell Swift’s object factory, “Build me an object of the type ChecklistItem.”
It is important to remember that just declaring that you have a variable does not automatically make the corresponding object for you. The variable is just the container for the object. You still have to instantiate the object and put it into the container. The variable is the box and the object is the thing inside the box.
So until you order an actual ChecklistItem object from the factory and put that into row0item, the variable is empty. And empty variables are a big no-no in Swift.
Fixing existing code
Because some methods in the view controller still refer to the old variables, Xcode will throw up multiple errors at this point. Before you can run the app again, you need to fix these errors. So, let’s do that now.
Note: You’re encouraged to type in the code from this book by hand (instead of copy-pasting), because that gives you a better feel for what you’re doing, but in the following instances it’s easier to just copy-paste from the PDF.
Unfortunately, copying from the PDF sometimes adds strange or invisible characters that confuse Xcode. It’s best to first paste the copied text into a plain text editor such as TextMate and then copy/paste from the text editor into Xcode.
Of course, if you’re reading the print edition of this book, copying & pasting from the book isn’t going to work, but you can still use copy-paste to save yourself some effort. Make the changes on one line and then copy that line to create the other lines. Copy-paste is a programmer’s best friend, but don’t forget to update the lines you pasted to use the correct variable names!
➤ In tableView(_:cellForRowAt:), replace the if statements with the following:
if indexPath.row == 0 {
label.text = row0item.text
} else if indexPath.row == 1 {
label.text = row1item.text
} else if indexPath.row == 2 {
label.text = row2item.text
} else if indexPath.row == 3 {
label.text = row3item.text
} else if indexPath.row == 4 {
label.text = row4item.text
}
➤ In tableView(_:didSelectRowAt:), again change the if statement block to:
if indexPath.row == 0 {
row0item.checked = !row0item.checked
} else if indexPath.row == 1 {
row1item.checked = !row1item.checked
} else if indexPath.row == 2 {
row2item.checked = !row2item.checked
} else if indexPath.row == 3 {
row3item.checked = !row3item.checked
} else if indexPath.row == 4 {
row4item.checked = !row4item.checked
}
➤ And finally, in configureCheckmark(for:at:), change the if block to:
if indexPath.row == 0 {
isChecked = row0item.checked
} else if indexPath.row == 1 {
isChecked = row1item.checked
} else if indexPath.row == 2 {
isChecked = row2item.checked
} else if indexPath.row == 3 {
isChecked = row3item.checked
} else if indexPath.row == 4 {
isChecked = row4item.checked
}
Basically, all of the above changes do one thing — instead of using the separate row0text and row0checked variables, you now use row0item.text and row0item.checked.
That takes care of all of the errors and you can even build and run the app. But if you do, you’ll notice that you get an empty table. Try clicking on the first five rows. You’ll notice that you get checkmarks toggling on and off for the first five rows. Curiouser, and curiouser… So what went wrong?
Setting up the objects
Remember how you read that the new row0item etc. variables are initialized with empty instances of ChecklistItem? That means that the text for each variable is empty. You still need to set up the values for these new variables!
➤ Modify viewDidLoad in ChecklistViewController.swift as follows:
override func viewDidLoad() {
super.viewDidLoad()
// Add the following lines
row0item.text = "Walk the dog"
row1item.text = "Brush my teeth"
row1item.checked = true
row2item.text = "Learn iOS development"
row2item.checked = true
row3item.text = "Soccer practice"
row4item.text = "Eat ice cream"
row4item.checked = true
}
This code simply sets up each of the new ChecklistItem variables that you created. If you’re wondering why some variables have a line to set the checked property and some don’t, remember that you initialize checked to false in the ChecklistItem class implementation. That default value is applied to the new object when you instantiate it. So, while you could still add a line to set checked to false, it isn’t necessary since the checked property is already set to false.
The above code is essentially doing the same thing as before, except that this time the text and checked variables are not separate instance variables of the view controller, but instead are properties of a ChecklistItem object.
➤ Run the app just to make sure that everything works now.
Putting the text and checked properties into their own ChecklistItem object already improved the code, but it is still a bit unwieldy.
Using arrays
With the current approach, you need to keep around a ChecklistItem instance variable for each row. That’s not ideal, especially if you want more than just a handful of rows.
Time to bring that array into play!
➤ In ChecklistViewController.swift, remove all the instance variables and replace them with a single array variable named items:
class ChecklistViewController: UITableViewController {
var items = [ChecklistItem]()
Instead of five different instance variables, one for each row, you now have just one variable for the array.
This looks similar to how you declared the previous variables but this time there are square brackets around ChecklistItem. Those square brackets indicate that the variable is going to be an array containing ChecklistItem objects. And the brackets at the end () simply indicate that you are creating an instance of this array — it will create an empty array with no items in the array.
➤ Modify viewDidLoad as follows:
override func viewDidLoad() {
super.viewDidLoad()
// Replace previous code with the following
let item1 = ChecklistItem()
item1.text = "Walk the dog"
items.append(item1)
let item2 = ChecklistItem()
item2.text = "Brush my teeth"
item2.checked = true
items.append(item2)
let item3 = ChecklistItem()
item3.text = "Learn iOS development"
item3.checked = true
items.append(item3)
let item4 = ChecklistItem()
item4.text = "Soccer practice"
items.append(item4)
let item5 = ChecklistItem()
item5.text = "Eat ice cream"
items.append(item5)
}
This is not that different from before, except that you now have to first create – or instantiate – each ChecklistItem object and add each instance to the array. Once the above code completes, the items array contains five ChecklistItem objects. This is your new data model.
Simplifying the code — again
Now that you have all your rows in the items array, you can simplify the table view data source and delegate methods once again.
➤ Change these methods:
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "ChecklistItem",
for: indexPath)
let item = items[indexPath.row] // Add this
let label = cell.viewWithTag(1000) as! UILabel
// Replace everything after the above line with the following
label.text = item.text
configureCheckmark(for: cell, at: indexPath)
return cell
}
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
// Replace everything inside this `if` condition
// with the following
let item = items[indexPath.row]
item.checked = !item.checked
configureCheckmark(for: cell, at: indexPath)
}
tableView.deselectRow(at: indexPath, animated: true)
}
func configureCheckmark(for cell: UITableViewCell,
at indexPath: IndexPath) {
// Replace full method implementation
let item = items[indexPath.row]
if item.checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
That’s a lot simpler than what you had before! Each method is now only a handful of lines long.
In each method, you do:
let item = items[indexPath.row]
This asks the array for the ChecklistItem object at the index that corresponds to the row number. Once you have that object, you can simply look at its text and checked properties and do whatever you need to do.
If the user were to add 100 to-do items to this list, none of this code would need to change. It works equally well with five items as with a hundred (or a thousand).
Speaking of the number of items, you can now change numberOfRowsInSection to return the actual number of items in the array, instead of a hard-coded number.
➤ Change the tableView(_:numberOfRowsInSection:) method to:
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return items.count
}
Not only is the code a lot shorter and easier to read, it can now also handle an arbitrary number of rows. That is the power of arrays!
➤ Run the app and see for yourself. It should still work exactly the same as before, but the internal structure of the code is way better.
Exercise: Add a few more rows to the table. You should only have to change
viewDidLoadfor this to work.
Cleaning up the code
There are a few more things you can do to improve the source code.
➤ Replace configureCheckmark(for:at:) with this one:
func configureCheckmark(for cell: UITableViewCell,
with item: ChecklistItem) {
if item.checked {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
Instead of an index-path, you now directly pass the ChecklistItem object to the method.
Note that now the full name of the method becomes configureCheckmark(for:with:) and that’s how you will call it from other places in the app.
Why did you change this method? Previously it received an index-path and then did the following to find the corresponding ChecklistItem:
let item = items[indexPath.row]
But in both cellForRowAt and didSelectRowAt you already do that. So, it’s simpler to pass that ChecklistItem object directly to configureCheckmark instead of making it do the same work twice. Anything that simplifies the code is good.
➤ Also add this new method:
func configureText(for cell: UITableViewCell,
with item: ChecklistItem) {
let label = cell.viewWithTag(1000) as! UILabel
label.text = item.text
}
This sets the checklist item’s text on the cell’s label. Previously you did that in cellForRowAt but it’s clearer to put that in its own method.
➤ Update tableView(_:cellForRowAt:) so that it calls these new methods:
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "ChecklistItem",
for: indexPath)
let item = items[indexPath.row]
configureText(for: cell, with: item)
configureCheckmark(for: cell, with: item)
return cell
}
➤ Also update tableView(_:didSelectRowAt:):
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
let item = items[indexPath.row]
item.toggleChecked()
configureCheckmark(for: cell, with: item)
}
tableView.deselectRow(at: indexPath, animated: true)
}
The above calls a new method named toggleChecked() on the item object instead of modifying the ChecklistItem’s checked property directly.
You will need to add this new method to the ChecklistItem object since Xcode should already be complaining about the method not being there.
➤ Open ChecklistItem.swift and add the following method (just below the property declarations and before the closing curly bracket):
func toggleChecked() {
checked.toggle()
}
Naturally, your own objects can also have methods. As you can see, this method does exactly what didSelectRowAt apart it’s not using Swift’s handy toggle() function which is basically doing checked = !checked dance for us.
A good object-oriented design principle is that you should let objects change their own state as much as possible. Previously, the view controller implemented this toggling behavior but now ChecklistItem knows how to toggle itself on or off.
➤ Run the app. It should still work exactly the same as before, but the code is a lot better. You can now have lists with thousands of to-do items, for those especially industrious users.
Clean up that mess!
So what’s the point of making all of these changes if the app still works exactly the same? For one, the code is much cleaner and that helps with avoiding bugs. By using an array you’ve also made the code more flexible. The table view can now handle any number of rows.
You’ll find that when you program you are constantly restructuring your code to make it better. It’s impossible to do the whole thing 100% perfect from the get go.
So you write code until it becomes messy and then you clean it up. After a little while it becomes a big mess again and you clean it up again. The process for cleaning up code is called refactoring and it’s a cycle that never ends.
There are a lot of programmers who never refactor their code. The result is what we call “spaghetti code” and it’s a horrible mess to maintain.
If you haven’t looked at your code for several months but need to add a new feature or fix a bug, you may need some time to read it through to understand again how everything fits together. This task becomes that much harder when you have spaghetti code.
So, it’s in your own best interest to write code that is as clean as possible.
If you want to check your work, you can find the project files for the current version of the app in the folder 10 - The Data Model in the Source Code folder.