Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

21. The Data Model
Written by Eli Ganim

In the previous chapter, you created a table view for the high scores and got it to display rows of items. However, this was all done using hard-coded, fake data. This would not do for a real high score screen since your users want to see their own real high scores up there.

To manage and display this information efficiently, you need a data model that allows you to store (and access) the high scores easily. 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 high scores data.

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 the high score screen, the high scores themselves 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:

How Model-View-Controller works
How Model-View-Controller works

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 the high score screen, 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 labels.

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. However, 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 an 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 FirstView or MainView. That is very confusing! If something is a view controller, its name should end with “ViewController,” not “View.” I sometimes wish Apple had left the word “view” out of “view controller” and just called it “controller” as that is a lot less misleading.

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 number. 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 table view controller (data source) gets the data from the model and puts it into the cells
The table view controller (data source) gets the data from the model and puts it into the cells

The data model for this app will be a list of high score items. Each of these items will get its own row in the table.

For each high score, you need to store two pieces of information: The name of the high scorer (Like “Manda”, “Adam”, etc) and the score.

That is two pieces of information per row, so you need two variables for each row.

The first iteration

First, you’ll see the cumbersome way to program this. It will work, but it isn’t very smart. Even though this is not the best approach, you should still 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 HighScoresViewController.swift, add the following constants right after the class HighScoresViewController line:

class HighScoresViewController: UITableViewController {
  let row0name = "The reader of this book"
  let row1name = "Manda"
  let row2name = "Joey"
  let row3name = "Adam"
  let row4name = "Eli"
  let row0score = 50000
  let row1score = 10000
  let row2score = 5000
  let row3score = 1000
  let row4score = 500
  . . .

These constants are defined outside of any method, they are not “local”, so they can be used by all of the methods in HighScoresViewController.

➤ 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: "HighScoreItem",
    for: indexPath)
  let nameLabel = cell.viewWithTag(1000) as! UILabel
  let scoreLabel = cell.viewWithTag(2000) as! UILabel

  if indexPath.row == 0 {
    nameLabel.text = row0name
    scoreLabel.text = String(row0score)
  } else if indexPath.row == 1 {
    nameLabel.text = row1name
    scoreLabel.text = String(row1score)
  } else if indexPath.row == 2 {
    nameLabel.text = row2name
    scoreLabel.text = String(row2score)
  } else if indexPath.row == 3 {
    nameLabel.text = row3name
    scoreLabel.text = String(row3score)
  } else if indexPath.row == 4 {
    nameLabel.text = row4name
    scoreLabel.text = String(row4score)
  }
  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 2 constants with the name and score for that row. Together, these 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.

Simplifying the code

Let’s combine the name and score into a new object of your own!

The object

➤ Select the Bullseye group in the project navigator and right-click it. Choose New File… from the pop-up menu.

Adding a new file to the project
Adding a new file to the project

Under the Source section, choose Swift File.

Click Next to continue. Save the new file as HighScoreItem. You don’t really need to add the .swift file extension since it will be automatically added for you.

Click Create to add the new file to the project.

➤ Add the following to the new HighScoreItem.swift file, below the import line:

class HighScoreItem {
  var name = ""
  var score = 0
}

The name property will store the name of the high scorer, the text that will appear in the table view cell’s label, and the score property will store the score.

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 HighScoreItem.swift for now. The HighScoreItem object currently only serves to combine the name and the score variables into one object. Later you’ll do more with it.

Using the object

Before you try using an array, you’ll replace the name and score instance variables in the view controller with these new HighScoreItem objects to see how that approach would work.

➤ In HighScoresViewController.swift, remove the old properties and replace them with HighScoreItem objects:

class HighScoresViewController: UITableViewController {
  var row0item = HighScoreItem()
  var row1item = HighScoreItem()
  var row2item = HighScoreItem()
  var row3item = HighScoreItem()
  var row4item = HighScoreItem()

These replace the row0name, row0score, etc. instance variables.

Wait a minute though… We’ve had variable declarations with a type, or with explicit 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 HighScoreItem. This creates an empty instance of HighScoreItem with the default values you defined when you added the class implementation — an empty string (””) for name and 0 for score.

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: HighScoreItem

If you did that, row0item won’t have a value yet, it would just be an empty container for a HighScoreItem object. And you’d still have to create the HighScoreItem instance later in your code. For example, in viewDidLoad.

The way you’ve done the code now, you initialize the variables above immediately with an empty instance of HighScoreItem 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 HighScoreItem.”

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.

Until you order an actual HighScoreItem 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: I generally encourage you 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, copy-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 {
    nameLabel.text = row0item.name
    scoreLabel.text = String(row0item.score)
  } else if indexPath.row == 1 {
    nameLabel.text = row1item.name
    scoreLabel.text = String(row1item.score)
  } else if indexPath.row == 2 {
    nameLabel.text = row2item.name
    scoreLabel.text = String(row2item.score)
  } else if indexPath.row == 3 {
    nameLabel.text = row3item.name
    scoreLabel.text = String(row3item.score)
  } else if indexPath.row == 4 {
    nameLabel.text = row4item.name
    scoreLabel.text = String(row4item.score)
  }

Basically, all of the above changes do one thing. Instead of using the separate row0name and row0score variables, you now use row0item.name and row0item.score.

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 a table with 5 zeros in it.

So what went wrong?

Setting up the objects

Remember how the new row0item etc. variables are initialized with empty instances of HighScoreItem? 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 HighScoreViewController.swift as follows:

override func viewDidLoad() {
  super.viewDidLoad()

  // Add the following lines
  row0item.name = "The reader of this book"
  row0item.score = 50000
  row1item.name = "Manda"
  row1item.score = 10000
  row2item.name = "Joey"
  row2item.score = 5000
  row3item.name = "Adam"
  row3item.score = 1000
  row4item.name = "Eli"
  row4item.score = 500
}

This code simply sets up each of the new HighScoreItem variables that you created. Essentially, it’s doing the same thing as before. Except, this time, the name and score variables are not separate instance variables of the view controller. Instead, they are properties of a HighScoreItem object.

➤ Run the app just to make sure that everything works now.

Putting the name and score properties into their own HighScoreItem object already improved the code, but it is still a bit unwieldy.

Using arrays

With the current approach, you need to keep around a HighScoreItem 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 HighScoresViewController.swift, remove all of the instance variables and replace them with a single array variable named items:

class HighScoresViewController: UITableViewController {
  var items = [HighScoreItem]()

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 HighScoreItem. Those square brackets indicate that the variable is going to be an array containing HighScoreItem 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 = HighScoreItem()
  item1.name = "The reader of this book"
  item1.score = 50000
  items.append(item1)
  
  let item2 = HighScoreItem()
  item2.name = "Manda"
  item2.score = 10000
  items.append(item2)
  
  let item3 = HighScoreItem()
  item3.name = "Joey"
  item3.score = 5000
  items.append(item3)
  
  let item4 = HighScoreItem()
  item4.name = "Adam"
  item4.score = 1000
  items.append(item4)
  
  let item5 = HighScoreItem()
  item5.name = "Eli"
  item5.score = 500
  items.append(item5)
}

This is not that different from before, except that you now have to first create — or instantiate — each HighScoreItem object and add each instance to the array. Once the above code completes, the items array contains five HighScoreItem 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 this methods:

override func tableView(_ tableView: UITableView,
             cellForRowAt indexPath: IndexPath) 
             -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(
                        withIdentifier: "HighScoreItem", 
                                   for: indexPath)
                 
  let item = items[indexPath.row]       // Add this
  
  let nameLabel = cell.viewWithTag(1000) as! UILabel
  let scoreLabel = cell.viewWithTag(2000) as! UILabel

  // Replace everything after the above line with the following
  nameLabel.text = item.name
  scoreLabel.text = String(item.score)
  return cell
}

That’s a lot simpler than what you had before! This method is now only a handful of lines long.

The most important part is the line:

let item = items[indexPath.row]

This asks the array for the HighScoreItem object at the index that corresponds to the row number. Once you have that object, you can simply look at its name and score properties and do whatever you need to do.

If the user were to add 100 high score 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 viewDidLoad for this to work.

If you want to check your work, you can find the project files for the current version of the app in the folder 21-The Data Model in the Source Code folder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.