Chapters

Hide chapters

UIKit Apprentice

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

17. Improved Data Model
Written by 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

Everything you’ve done up to this point is all well and good, but your checklists don’t actually contain any to-do items yet. Or rather, if you select a checklist, you see the same old items for every list! There is no connection between the selected list and the items displayed for that list.

It’s time for you to fix that. You’ll do so by way of the following steps:

  • The new data model: Update the data model so that the to-do items for a list are saved along with the list.
  • Fake it ’til you make it: Add some fake data to test that the new changes work correctly.
  • Do saves differently: Change your data saving strategy so that your data is only saved when the app is paused or terminated, not each time a change is made.
  • Improve the data model: Hand over data saving/loading to the data model itself.

The new data model

So far, the list of to-do items and the actual checklists have been separate from each other.

Let’s change the data model to look like this:

Each Checklist object has an array of ChecklistItem objects
Each Checklist object has an array of ChecklistItem objects

There will still be the lists array that contains all the Checklist objects, but each of these Checklist instances will have its own array of ChecklistItem objects.

The to-do item array

➤ Add a new property to Checklist.swift:

class Checklist: NSObject {
  var name = ""
  var items = [ChecklistItem]()     // add this line
  . . .
var items: [ChecklistItem] = [ChecklistItem]()
var items: [ChecklistItem] = []
var items = []

Pass the array

Earlier you fixed prepare(for:sender:) in AllListsViewController.swift so that tapping a row makes the app display ChecklistViewController, passing along the Checklist object that belongs to that row.

override func tableView(
  _ tableView: UITableView, 
  numberOfRowsInSection section: Int
) -> Int {
  return checklist.items.count
}
override func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  . . .
  let item = checklist.items[indexPath.row]
  . . .
}
override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  . . .
  let item = checklist.items[indexPath.row]
  . . . 
}
override func tableView(
  _ tableView: UITableView, 
  commit editingStyle: UITableViewCellEditingStyle, 
  forRowAt indexPath: IndexPath
) {
  checklist.items.remove(at: indexPath.row)
  . . .
}
func itemDetailViewController(
  _ controller: ItemDetailViewController, 
  didFinishAdding item: ChecklistItem
) {
  let newRowIndex = checklist.items.count
  checklist.items.append(item)
  . . .
}
func itemDetailViewController(
  _ controller: ItemDetailViewController, 
  didFinishEditing item: ChecklistItem
) {
  if let index = checklist.items.firstIndex(of:item) {
  . . .
}
override func prepare(
  for segue: UIStoryboardSegue, 
  sender: Any?
) {
    . . .
    controller.itemToEdit = checklist.items[indexPath.row]
    . . .
}

Fake it ‘til you make it

Let’s add some fake data to the various Checklist objects so that you can test whether this new design actually works.

Add fake to-do data

In AllListsViewController’s viewDidLoad() you already put fake Checklist objects into the lists array. It’s time to add something new to this method.

// Add placeholder item data
for list in lists {
  let item = ChecklistItem()
  item.text = "Item for \(list.name)"
  list.items.append(item)
}

Programming language constructs

For the sake of review, let’s go over the programming language stuff you’ve already seen. Most modern programming languages offer at least the following basic building blocks:

The for loop

Let’s go through that for loop line-by-line:

for list in lists {
  . . . 
}
let item = ChecklistItem()
item.text = "Item for \(list.name)"
list.items.append(item)
var item = ChecklistItem()
item.text = "Item for Birthdays"
lists[0].items.append(item)

item = ChecklistItem()
item.text = "Item for Groceries"
lists[1].items.append(item)

item = ChecklistItem()
item.text = "Item for Cool Apps"
lists[2].items.append(item)

item = ChecklistItem()
item.text = "Item for To Do"
lists[3].items.append(item)
Each Checklist now has its own items
Oikc Dzacqcuqz xuc fem ajj asq ibilv

The new load/save code

Let’s put the load/save code back in. This time you’ll make AllListsViewController do the loading and saving. Yes, I know I said that Checklist should handle its own loading/saving and we’ll get to that soon …

// MARK: - Data Saving
func documentsDirectory() -> URL {
  let paths = FileManager.default.urls(
    for: .documentDirectory, 
    in: .userDomainMask)
  return paths[0]
}

func dataFilePath() -> URL {
  return documentsDirectory().appendingPathComponent("Checklists.plist")
}

// this method is now called saveChecklists()
func saveChecklists() {
  let encoder = PropertyListEncoder()
  do {
    // You encode lists instead of "items"
    let data = try encoder.encode(lists)
    try data.write(
      to: dataFilePath(), 
      options: Data.WritingOptions.atomic)
  } catch {
    print("Error encoding list array: \(error.localizedDescription)")
  }
}

// this method is now called loadChecklists()
func loadChecklists() {
  let path = dataFilePath()
  if let data = try? Data(contentsOf: path) {
    let decoder = PropertyListDecoder()
    do {
      // You decode to an object of [Checklist] type to lists
      lists = try decoder.decode(
        [Checklist].self, 
        from: data)
    } catch {
      print("Error decoding list array: \(error.localizedDescription)")
    }
  }
}
override func viewDidLoad() {
  super.viewDidLoad()
  navigationController?.navigationBar.prefersLargeTitles = true
  tableView.register(
    UITableViewCell.self, 
    forCellReuseIdentifier: cellIdentifier)
  // Load data
  loadChecklists()
}
class Checklist: NSObject, Codable {

Do saves differently

Previously, you saved the data whenever the user changed something: adding a new item, deleting an item, and toggling a checkmark all caused Checklists.plist to be re-saved. That used to happen in ChecklistViewController.

Parents and their children

The terms parent and child are common in software development.

The new saving strategy

You may think: ah, I could use a delegate for this. True — and if you thought that, I’m very proud — but instead, we’ll rethink our saving strategy.

Save changes on app termination

The ideal place for handling app termination notifications is inside the scene delegate. You haven’t spent much time with this object before, but every app has one. Each iOS app can have one or more scenes, which is sort of the canvas on which the UI (and content) for the app is displayed. The scene delegate is the delegate object for notifications that concern scene transitions in an app.

func sceneDidDisconnect(_ scene: UIScene)
func sceneDidEnterBackground(_ scene: UIScene)
// MARK: - Helper Methods
func saveData() {
  let navigationController = window!.rootViewController as! UINavigationController
  let controller = navigationController.viewControllers[0] as! AllListsViewController
  controller.saveChecklists()
}

Unwrapping optionals

At the top of SceneDelegate.swift you can see that window is declared as an optional:

var window: UIWindow?
if let w = window {
  // if window is not nil, w is the real UIWindow object
  let navigationController = w.rootViewController
}
let navigationController = window?.rootViewController
let navigationController = window!.rootViewController
The navigation controller is the window’s root view controller
Rva vovilifouh texcqinzog ip zva wubjot’q caet loay mivgxorgaw

let controller = navigationController.viewControllers[0] as! AllListsViewController
From the root view controller to the AllListsViewController
Cdog wzo joiw jiap tosgsadlah su jse AjyGisjfHaawNetngussol

func sceneDidDisconnect(_ scene: UIScene) {
  saveData()
}

func sceneDidEnterBackground(_ scene: UIScene) {
  saveData()
}

Improve the data model

The previous code works, but you can still do a little better. You have made data model objects for Checklist and ChecklistItem but the code for loading and saving the Checklists.plist file currently lives in AllListsViewController. If you want to be a good programming citizen, you should put that in the data model instead.

The DataModel class

I prefer to create a top-level DataModel object for many of my apps. For this app, DataModel will contain the array of Checklist objects. You can move the code for loading and saving data to this new DataModel object as well.

import Foundation

class DataModel {
  var lists = [Checklist]()
}
init() {
  loadChecklists()
}
var dataModel: DataModel!

Create the DataModel object

But where/how does the dataModel instance variable get populated? There is no place in the code that currently says dataModel = DataModel().

let dataModel = DataModel()
func saveData() {
  dataModel.saveChecklists()
}
func scene(
  _ scene: UIScene, 
  willConnectTo session: UISceneSession, 
  options connectionOptions: UIScene.ConnectionOptions
) {
  let navigationController = window!.rootViewController as! UINavigationController
  let controller = navigationController.viewControllers[0] as! AllListsViewController
  controller.dataModel = dataModel
}

Still confused about var and let?

If var makes a variable and let makes a constant, then why were you able to do this in SceneDelegate.swift:

let dataModel = DataModel()
let i = 100
i = 200       // not allowed
i += 1        // not allowed

var j = 100
j = 200       // allowed
j += 1        // allowed
var s = "hello"
var u = s         // u has its own copy of "hello"
s += " there"     // s and u are now different
var d = DataModel()
var e = d                 // e refers to the same object as d
d.lists.remove(at: 0)     // this also changes e
let d = DataModel()
let e = d                 // e refers to the same object as d
d.lists.remove(at: 0)     // this also changes e
let d = DataModel()
d = someOtherDataModel   // error: cannot change the reference
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