Chapters

Hide chapters

UIKit Apprentice

First Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

10. The Data Model
Written by Matthijs Hollemans & Fahim Farook

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

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 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 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 checkmark that can be on or off.

The table view controller (data source) gets the data from the model and puts it into the cells
Fwe terhi poaw qokwharneh (wemu luejga) japs mqe kohu msor dtu lirik ady cigl id axqi dqi cethq

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.

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"
  . . .
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
}

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.

var row0checked = false
var row1checked = false
var row2checked = false
var row3checked = false
var row4checked = false
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)
}
    if indexPath.row == 0 {
      row0checked = !row0checked
      if row0checked {
        cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none
      }
    } . . .
      row0checked = !row0checked
      if row0checked {
        cell.accessoryType = .checkmark
      } else {
        cell.accessoryType = .none
      }

Toggling Booleans

The toggling of Bool values from true to false (or vice versa) is such a common action, that Swift has added a method to do this easily without worrying what the Bool variable’s value is. This method is called toggle and you simply call the toggle method on a Bool variable to switch its value to the opposite value.

override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  if let cell = tableView.cellForRow(at: indexPath) {
    var isChecked = false

    if indexPath.row == 0 {
       row0checked.toggle()
       isChecked = row0checked
     } else if indexPath.row == 1 {
       row1checked.toggle()
       isChecked = row1checked
     } else if indexPath.row == 2 {
       row2checked.toggle()
       isChecked = row2checked
     } else if indexPath.row == 3 {
       row3checked.toggle()
       isChecked = row3checked
     } else if indexPath.row == 4 {
       row4checked.toggle()
       isChecked = row4checked
     }
     if isChecked {
       cell.accessoryType = .checkmark
     } else {
       cell.accessoryType = .none
     }
   }    
  tableView.deselectRow(at: indexPath, animated: true)
}
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
  }
}
override func tableView(
  _ tableView: UITableView, 
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  . . . 

  configureCheckmark(for: cell, at: indexPath)
  return cell
}

External and internal parameter names

The new configureCheckmark method has two parameters, for and at. Its full name is therefore configureCheckmark(for:at:).

configureCheckmark(for: someCell, at: someIndexPath)
configureCheckmark(someCell, someIndexPath)
func configureCheckmark(
  for cell: UITableViewCell, 
  at indexPath: IndexPath
) {
  if indexPath.row == 0 {
    . . . 
  } 

  cell.accessoryType = .checkmark
  . . . 
}

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

override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  if let cell = tableView.cellForRow(at: indexPath) {
    if indexPath.row == 0 {
      row0checked.toggle()
    } else if indexPath.row == 1 {
      row1checked.toggle()
    } else if indexPath.row == 2 {
      row2checked.toggle()
    } else if indexPath.row == 3 {
      row3checked.toggle()
    } else if indexPath.row == 4 {
      row4checked.toggle()
    }
    configureCheckmark(for: cell, at: indexPath)
  }
  tableView.deselectRow(at: indexPath, animated: true)
}
var row0checked = false
var row1checked = true
var row2checked = true
var row3checked = false
var row4checked = true
The data model and the table view cells are now always in-sync
Yji zobi viquq ijz svo donqo miib fopll ehe bol annitj uw-gqgl

Arrays

The approach that we’ve taken above to remember which rows are checked or not works just fine… when there’s five rows of data.

Arrays are ordered lists containing multiple objects
Emsalv igu ukvulud zuksw nupreiluvv hoyduvku igrizdn

Arrays can also include other arrays
Ocserv boy uzsu awsguvu abtot irkeww

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 popup menu:

Adding a new file to the project
Ettogc e van qafi za nde kfeqagp

Choosing the Swift File class template
Qboisavv sco Zyony Zoci ttahc bizhwote

Saving the new Swift file
Qigarj stu cup Hzanz soka

class ChecklistItem {
  var text = ""
  var checked = false
}

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.

class ChecklistViewController: UITableViewController {
  var row0item = ChecklistItem()
  var row1item = ChecklistItem()
  var row2item = ChecklistItem()
  var row3item = ChecklistItem()
  var row4item = ChecklistItem()
var row0item: ChecklistItem

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.

  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
  }
    if indexPath.row == 0 {
      row0item.checked.toggle()
    } else if indexPath.row == 1 {
      row1item.checked.toggle()
    } else if indexPath.row == 2 {
      row2item.checked.toggle()
    } else if indexPath.row == 3 {
      row3item.checked.toggle()
    } else if indexPath.row == 4 {
      row4item.checked.toggle()
    }
  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
  }

Set up the objects

Remember how I said 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!

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
}

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.

class ChecklistViewController: UITableViewController {
  var items = [ChecklistItem]()
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)
}

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

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

    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
  }
}
let item = items[indexPath.row]
override func tableView(
  _ tableView: UITableView, 
  numberOfRowsInSection section: Int
) -> Int {
  return items.count
}

Clean up the code

There are a few more things you can do to improve the source code.

func configureCheckmark(
  for cell: UITableViewCell, 
  with item: ChecklistItem
) {
  if item.checked {
    cell.accessoryType = .checkmark
  } else {
    cell.accessoryType = .none
  }
}
let item = items[indexPath.row]
func configureText(
  for cell: UITableViewCell, 
  with item: ChecklistItem
) {
  let label = cell.viewWithTag(1000) as! UILabel
  label.text = item.text
}
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
}
override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {  
  if let cell = tableView.cellForRow(at: indexPath) {
    let item = items[indexPath.row]
    item.checked.toggle()
    configureCheckmark(for: cell, with: item)
  }
  tableView.deselectRow(at: indexPath, animated: true)
}
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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now