Chapters

Hide chapters

UIKit Apprentice

Third Edition · iOS 18 · Swift 5.10 · Xcode 16

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

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:

Checklist 
object Checklist 
object Checklist 
object Checklist 
Item Checklist 
Item Checklist 
Item Checklist 
Item Checklist 
Item Checklist 
Item Checklist 
Item Checklist 
Item Checklist 
Item lists array items
array items
array items
array
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
  . . .

This creates a new empty array that can hold ChecklistItem objects and assigns it to the items property.

If you’re a stickler for completeness, you can also write it as follows:

var items: [ChecklistItem] = [ChecklistItem]()

I personally don’t like this way of declaring variables because it violates the “DRY” principle – Don’t Repeat Yourself. Fortunately, thanks to Swift’s type inference, you can save yourself some keystrokes. Another way you’ll see it written sometimes is:

var items: [ChecklistItem] = []

The notation [] means: make an empty array of the specified type. There is no type inference at play there since you have to specify the type explicitly. If you don’t specify a type and write the above line as:

var items = []

You will get an error since the compiler cannot determine the type of the array. That makes sense, right? Regardless of the way you choose to write it, the Checklist object now contains an array of ChecklistItem objects. Initially, that array is empty.

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.

Currently ChecklistViewController still gets the ChecklistItem objects that it displays from its own private items array. You will change that so it reads from the items array inside the Checklist object instead.

➤ Remove the items instance variable from ChecklistViewController.swift.

➤ Then, anywhere it says items, change it to say checklist.items instead.

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

➤ Delete the following methods from ChecklistViewController.swift.

Tip: You may want to set aside the code from these methods in a temporary file somewhere; shortly you’ll be using them again in a slightly modified form.

  • func documentsDirectory()
  • func dataFilePath()
  • func saveChecklistItems()
  • func loadChecklistItems()

You added these methods to load and save the checklist items from a file. That is no longer the responsibility of this view controller. It is better, from a design perspective, for the Checklist object to do that.

Loading and saving data model objects really belongs in the data model itself, rather than in a controller.

But before you get to that, let’s first test whether these changes were successful. Xcode is throwing up a few errors because you still call saveChecklistItems() and loadChecklistItems() from several places in the code. You should remove those lines, as you will soon be saving the items from a different place.

➤ Remove the lines that call saveChecklistItems() and loadChecklistItems().

➤ Press ⌘+B to make sure the app builds without errors.

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 the following to the bottom of AllListsViewController.swift’s viewDidLoad():

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

This introduces something you haven’t seen before: the for in statement. Like if, this is a special language construct.

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 ability to remember values by storing things into variables. Some variables are simple, such as Int and Bool. Others can store objects — ChecklistItem, UIButton — or even collections of objects such as Array.

  • The ability to read values from variables and use them for basic arithmetic — multiply, add — and comparisons — greater than, not equals, etc.

  • The ability to make decisions. You’ve already seen the if statement, but there is also a switch statement that is shorthand for if with many else ifs.

  • The ability to group functionality into units such as methods and functions. You can call those methods and receive back a result value that you can then use in further computations.

  • The ability to bundle functionality (methods) and data (variables) together into objects.

  • The ability to execute one or more lines of code inside a do block and to catch any errors thrown via a try statement. Or, to simply bypass the do block by using a try? statement instead.

  • The ability to repeat a set of statements more than once. This is what the for in statement does. There are other ways to perform repetitions as well: while and repeat. Endlessly repeating things is what computers are good at.

Everything else is built on top of these building blocks. You’ve seen most of these already, but repetitions (or loops in programmer talk) are new.

If you grok the concepts from this list, you’re well on your way to becoming a software developer. And if not, well, just hang in there!

The for loop

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

for list in lists {
  . . .
}

This means the following: for every Checklist object in the lists array, perform the statements between the curly braces.

The first time through the loop, the temporary list variable will hold a reference to the Birthdays checklist, as that is the first Checklist object that you created and added to the lists array.

Inside the loop you do:

let item = ChecklistItem()
item.text = "Item for \(list.name)"
list.items.append(item)

This should be familiar. You first create a new ChecklistItem object. Then you set its text property to “Item for Birthdays” because the \(…) placeholder gets replaced with the name of the Checklist object, list.name, which is “Birthdays”.

Finally, you add this new ChecklistItem to the Birthdays Checklist object, or rather, to its items array.

That concludes the first pass through this loop. Now the for in statement will look at the lists array again and sees that there are three more Checklist objects in that array. So it puts the next one, Groceries, into the list variable and the process repeats.

This time the text is “Item for Groceries”, which is put into its own ChecklistItem object that goes into the items array of the Groceries Checklist object.

After that, the loop adds a new ChecklistItem with the text “Item for Cool Apps” to the Cool Apps checklist, and “Item for To Do” to the To Do checklist.

Then there are no more objects left to look at in the lists array and the loop ends.

Using loops will often save you a lot of time. You could have written this code as follows:

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)

That’s very repetitive, which is a good sign it’s better to use a loop. Imagine if you had 100 Checklist objects… would you be willing to copy-paste that code a hundred times? I’d rather use a loop.

Most of the time you won’t even know in advance how many objects you’ll have, so it’s impossible to write it all out by hand. By using a loop you don’t need to worry about that. The loop will work just as well for three items as for three hundred.

As you can imagine, loops and arrays work quite well together.

➤ Run the app. You’ll see that each checklist now has its own set of items.

Play with it for a minute, remove items, add items, and verify that each list indeed is completely separate from the others.

Each Checklist now has its own items
Each Checklist now has its own items

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 …

➤ Add the following to AllListsViewController.swift — you may want to copy this from that temporary file you might (or might not) have created, but be sure to make the changes mentioned in the comments:

// 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)")
    }
  }
}

This is mostly identical to what you had before in ChecklistViewController, except that you load and save the lists array instead of the items array. Note that the decode type is now [Checklist].self instead of [ChecklistItem].self. Also, the names of the methods changed slightly, as well as the error messages for the catch blocks.

➤ Change viewDidLoad() to:

override func viewDidLoad() {
  super.viewDidLoad()
  navigationController?.navigationBar.prefersLargeTitles = true
  tableView.register(
    UITableViewCell.self,
    forCellReuseIdentifier: cellIdentifier)
  // Load data
  loadChecklists()
}

This gets rid of the test data you put there earlier and makes the loadChecklists() method do all the work.

You also have to make the Checklist object support the Codable protocol — but as you know, that’s just a simple change.

➤ Add the Codable protocol in Checklist.swift:

class Checklist: NSObject, Codable {

Important: Before you run the app, remove the old Checklists.plist file from the Simulator’s Documents folder.

If you don’t, the app will most probably throw up an error message in the Console about a decoding error because the internal format of the file no longer corresponds to the new data you’re loading and saving. This is because the Swift Codable protocol handles data encoding/decoding in a safe fashion.

Weird crashes

When I first wrote this book, I didn’t think to remove the Checklists.plist file before running the app. That was a mistake, but the app appeared to work fine… until I added a new checklist. At that point the app aborted with a strange error message from UITableView that made no sense at all.

I started to wonder whether I tested the code properly. But then I thought of the old file, removed it and ran the app again. It worked perfectly. Just to make sure it was the fault of that file, I put a copy of the old file back and ran the app again. Sure enough, when I tried to add a new checklist it crashed.

The explanation for this kind of error is that somehow the code managed to load the old file, even though its format no longer corresponded to the new data model. This put the table view into a bad state. Any subsequent operations on the table view caused the app to crash. Do note though that this was before Codable when the book used a different mechanism for saving/loading data.

You’ll run into this type of bug every so often, where the crash isn’t directly caused by what you’re doing but by something that went wrong earlier on. These kinds of bugs can be tricky to solve, because you can’t fix them until you find the true cause.

There is a section devoted to debugging techniques towards the end of the book because it’s inevitable that you’ll introduce bugs in your code. Knowing how to find and eradicate bugs is an essential skill that any programmer should master – if only to save you a lot of time and aggravation!

➤ Run the app and add a checklist and a few to-do items.

➤ Exit the app using the Xcode Stop button and run it again. You’ll see that the list is empty again. All your to-do items are gone!

You can add all the checklists and items you want, but nothing gets saved anymore. What’s going on here?

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.

However, you just moved the saving logic to AllListsViewController. How do you make sure changes to the to-do items get saved now? The AllListsViewController doesn’t know when a checkmark is toggled on or off.

You could give ChecklistViewController a reference to the AllListsViewController and have it call its saveChecklists() method whenever the user changes something, but that introduces a child-parent dependency and you’ve been trying hard to avoid those (ownership cycles, remember?).

Parents and their children

The terms parent and child are common in software development.

A parent is an object higher up in some hierarchy; a child is an object lower in the hierarchy.

In this case, the “hierarchy” represents the navigation flow between the different screens of the app.

The All Lists screen is the parent of the Checklist screen, because All Lists was “born” first. It creates a new ChecklistViewController “baby” every time the user views the item list for a checklist.

Likewise, All Lists is also the parent of the List Detail screen. The Item Detail screen, however, is the child of the Checklist view controller.

Navigation
Controller All Lists parent parent parent parent child child child Checklist List Detail Item 
Detail child

Generally speaking, it’s OK if the parent knows everything about its children, but not the other way around — just like in real life, every parent has horrible secrets they don’t want their kids to know about… or so I’ve been told.

As a result, you don’t want parent objects to be dependent on their child objects, but the other way around is fine. So ChecklistViewController asking AllListsViewController to do things is a big no-no.

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.

Is it really necessary to save changes all the time? While the app is running, the data model sits in working memory and is always up-to-date.

The only time you have to load anything from the file (the long-term storage memory) is when the app first starts up, but never afterwards. From then on you always make the changes to the objects in the working memory.

But when changes are made, the file becomes out-of-date. That is why you save those changes – to keep the file in sync with what is in memory.

The reason you save to a file is so that you can restore the data model in working memory after the app gets terminated. But until that happens, the data in the short-term working memory will do just fine.

You just need to make sure that you save the data to the file just before the app is terminated. In other words, the only time you save is when you actually need to keep the data safe.

Not only is this more efficient, especially if you have a lot of data, it also is simpler to program. You no longer need to worry about saving every time the user makes a change to the data, only right before the app terminates.

There are three situations in which an app can terminate:

  1. While the user is running the app. This doesn’t happen very often anymore, but earlier versions of iOS did not support multitasking apps. Receiving an incoming phone call, for example, would kill the currently running app. As of iOS 4, the app will simply be suspended and sent to the background when that happens.

    There are still situations where iOS may forcefully terminate a running app, for example, if the app becomes unresponsive or runs out of memory.

  2. When the app is suspended. Most of the time iOS keeps running apps around for a long time. Their data is frozen in memory and no computations are taking place. When you resume a suspended app, it literally continues from where it left off.

    Sometimes the OS needs to make room for an app that requires a lot of working memory — often a game — and then it simply kills the suspended apps and wipes them from memory. The suspended apps are not notified when this happens.

  3. The app crashes. There are ways to detect crashes, but handling them can be very tricky. Trying to deal with the crash may actually make things worse. The best way to avoid crashes is to make no programming mistakes! :]

Fortunately for us, iOS will inform the app about significant changes such as, “you are about to be terminated”, and, “you are about to be suspended”. You can listen for these events and save your data at that point. That will ensure the on-file representation of the data model is always up-to-date when the app does terminate.

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.

This is where you receive the “scene will terminate” and “scene will be suspended” notifications.

In fact, if you look inside SceneDelegate.swift, you’ll see the methods:

func sceneDidDisconnect(_ scene: UIScene)

And:

func sceneDidEnterBackground(_ scene: UIScene)

There are a few others, but these are the ones you need. The Xcode template puts helpful comments inside these methods, so you know what to do with them.

Now the trick is, how do you call AllListsViewController’s saveChecklists() method from these delegate methods? The scene delegate does not know anything about AllListsViewController — at least, not yet.

You have to use some trickery to find the All Lists View Controller from within the scene delegate.

➤ Add this new method to SceneDelegate.swift:

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

The saveData() method looks at the scene’s — or rather, the scene delegate’s — window property to find the UIWindow object that contains the full UI hierarchy for your app.

UIWindow is the top-level container for all your app’s views. There is only one UIWindow object per scene in your iOS app. On iOS, when you want to have multiple windows, you need to create additional scenes. But that’s not something we will concentrate on at this point.

Exercise: Can you explain why you wrote window! with an exclamation point?

Unwrapping optionals

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

var window: UIWindow?

To unwrap an optional you normally use the if let syntax:

if let w = window {
  // if window is not nil, w is the real UIWindow object
  let navigationController = w.rootViewController
}

As a shorthand you can use optional chaining:

let navigationController = window?.rootViewController

If window is nil, then the app won’t even bother to look at the rest of the statement and navigationController will also be nil.

For apps that use a storyboard — and quite a lot of them do —, you’re guaranteed that window is never nil, even though it is an optional. UIKit promises that it will put a valid reference to the app’s UIWindow object inside the window variable when the app starts up.

So why is it an optional? There is a brief moment between when the app is launched and the storyboard is loaded where the window property does not have a valid value yet. And if a variable can be nil – no matter how briefly – then Swift requires it to be an optional.

If you’re sure an optional will not be nil when you’re going to use it, you can force unwrap it by adding an exclamation point:

let navigationController = window!.rootViewController

That’s exactly what you’re doing in the saveData() method. Force unwrapping is the simplest way to deal with optionals, but it comes with some danger: if you’re wrong and the optional is nil, the app will crash. Use with caution!

You’ve actually used force unwrapping already when you read the text from the UITextField objects in the Item Detail and List Detail view controllers. The UITextField text property is an optional String but it will never be nil, which is why you can read it with textField.text! – the exclamation point converts the optional String value to a regular String.

Normally you don’t need to do anything with your UIWindow, but in cases such as this, you ask it for its rootViewController. The “root” or “initial” view controller is the very first scene from the storyboard — the navigation controller all the way over on the left.

You can see this in Interface Builder where the navigation controller has a big arrow pointing at it:

The navigation controller is the window’s root view controller
The navigation controller is the window’s root view controller

The Attributes inspector for this navigation controller also has the Is Initial View Controller box checked, that’s the same thing. In the Document Outline the arrow is called the Storyboard Entry Point.

Once you have the navigation controller, you can find the AllListsViewController. After all, that’s the view controller that is embedded in the navigation controller.

Unfortunately, the UINavigationController does not have a “rootViewController” property of its own, so you have to look into its viewControllers array to find the AllListsViewController:

let controller = navigationController.viewControllers[0] as! AllListsViewController

As usual, a type cast is necessary because the viewControllers array does not know anything about the exact types of your own view controllers. Once you have a reference to AllListsViewController you can call its saveChecklists() method.

It’s a bit of work to dig through the window and navigation controller to find the view controller you need, but that’s life as an iOS developer.

UIWindow All ListsView Controller UINavigation Controller rootViewController func saveChecklists() { ... } viewController - 1 2 0
From the root view controller to the AllListsViewController

Note: By the way, the UINavigationController does have a topViewController property, but you cannot use it here: the “top” view controller is the screen that is currently displaying, which may be the ChecklistViewController if the user is looking at to-do items. You don’t want to send the saveChecklists() message to that screen — it has no method to handle that message and the app will crash!

➤ Change the sceneDidEnterBackground(_:) and sceneDidDisconnect(_:) methods in SceneDelegate.swift to call saveData():

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

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

➤ Run the app, add some checklists, add items to those lists, and set some checkmarks.

➤ Press the Simulator’s home button, or press Shift+⌘+H, or pick Device ▸ Home from the Simulator’s menu bar, to make the app go to the background. This simulates what happens when a user taps the home button on their iPhone.

Look inside the app’s Documents folder using Finder. There should be a new Checklists.plist file there.

➤ Press Stop in Xcode to terminate the app. Run the app again and your data should still be there. Awesome!

Xcode’s Stop button

Important note: When you press Xcode’s Stop button, the scene delegate will not receive the sceneDidDisconnect(_:) notification. Xcode kills the app immediately, without mercy.

Therefore, to test the saving behavior, always simulate a tap on the home button to make the app go into the background before you press Stop. If you don’t to that, you’ll lose your data. Caveat developer.

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.

➤ Add a new file to the project using the New Empty File context menu option and name it DataModel.swift.

➤ Add the following code to DataModel.swift:

import Foundation

class DataModel {
  var lists = [Checklist]()
}

This defines the new DataModel object and gives it a lists property.

Unlike Checklist and ChecklistItem, DataModel does not need to be built on top of NSObject. It also does not need to conform to the Codable protocol since we will not be serializing DataModel objects, just the array of Checklist instances that a DataModel instance holds.

DataModel will take over the responsibilities for loading and saving the to-do lists from AllListsViewController.

➤ Cut the following methods out of AllListsViewController.swift and paste them into DataModel.swift:

  • func documentsDirectory()
  • func dataFilePath()
  • func saveChecklists()
  • func loadChecklists()

➤ Add an init() method to DataModel.swift:

init() {
  loadChecklists()
}

This makes sure that as soon as the DataModel object is created, it will attempt to load Checklists.plist.

You don’t have to call super.init() because DataModel does not have a superclass – it is not built on NSObject or any other existing class.

Switch to AllListsViewController.swift and make the following changes:

➤ Remove the lists instance variable.

➤ Remove the call to loadChecklists() in viewDidLoad.

➤ Add a new instance variable:

var dataModel: DataModel!

The ! is necessary because dataModel will temporarily be nil when the app starts up. It doesn’t have to be a true optional – with ? – because once dataModel is given a value, it will never become nil again.

You might be tempted to simply create a new instance of DataModel in the above line instead of declaring an instance variable which has to be populated later. There’s a good reason for not doing it this way. You’ll soon see why.

Xcode will find a number of errors in AllListsViewController.swift. You can no longer reference the lists variable directly, because it no longer exists. Instead, you’ll have to ask the DataModel for its lists property.

➤ Wherever the code for AllListsViewController says lists, replace it with dataModel.lists. You need to do this in the following methods:

  • tableView(_:numberOfRowsInSection:)
  • tableView(_:cellForRowAt:)
  • tableView(_:didSelectRowAt:)
  • tableView(_:commit:forRowAt:)
  • tableView(_:accessoryButtonTappedForRowWith:)
  • listDetailViewController(_:didFinishAdding:)
  • listDetailViewController(_:didFinishEditing:)

Phew, that’s a big list! Fortunately, the change is very simple.

To recap, you created a new DataModel object that owns the array of Checklist objects and knows how to load and save the checklists and their items.

Instead of its own array, the AllListsViewController now uses this DataModel object, which it accesses through the dataModel property.

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

That’s because the best place for that is in the scene delegate. You can consider the scene delegate to be the top-level object in your app. Therefore, it makes sense to make it the “owner” of the data model. Plus, since you do the data saving in the scene delegate, you need a reference to the data model from the scene delegate anyway. So it makes sense to create the data model instance in the scene delegate and then pass it on to any view controllers that needs it.

➤ In SceneDelegate.swift, add a new property:

let dataModel = DataModel()

This creates the DataModel object and puts it in a constant named dataModel.

Even though AllListsViewController also has an instance variable named dataModel, these two things are totally separate from each other. Here you’re only putting the DataModel object into SceneDelegate’s dataModel property.

➤ Simplify the saveData() method to just this:

func saveData() {
  dataModel.saveChecklists()
}

If you run the app now, it will crash at once because AllListsViewController’s own reference to DataModel is still nil. I told you those nils were no-gooders!

The best place to share the DataModel instance with AllListsViewController is in the scene(_:willConnectTo:connectionOptions:) method, which gets called as soon as the app starts up.

➤ Change that method to:

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
}

This finds the AllListsViewController by looking in the storyboard as before and then sets its dataModel property. Now the All Lists screen can access the array of Checklist objects again.

➤ Run the app again and verify that everything still works. It does? Great!

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

You’d think that when something is constant it cannot change, right? Then how come the app lets you add new Checklist objects to DataModel? Obviously the DataModel object can be changed…

Here’s the thing: Swift makes a distinction between value types and reference types, and let works differently for value types as opposed to reference types.

An example of a value type is Int. Once you create a constant of type Int you can never change it afterwards:

let i = 100
i = 200       // not allowed
i += 1        // not allowed

var j = 100
j = 200       // allowed
j += 1        // allowed

The same goes for other value types such as Float, String, and even Array. They are called value types because the variable or constant directly stores their value.

When you assign the contents of one variable to another, the value is copied into the new variable:

var s = "hello"
var u = s         // u has its own copy of "hello"
s += " there"     // s and u are now different

But objects that you define with the keyword class — such as DataModel — are reference types. The variable or constant does not contain the actual object, only a reference to the object — the reference is simply the memory location where the object is stored.

var d = DataModel()
var e = d                 // e refers to the same object as d
d.lists.remove(at: 0)     // this also changes e

You can also write this using let and it would do the exact same thing:

let d = DataModel()
let e = d                 // e refers to the same object as d
d.lists.remove(at: 0)     // this also changes e

So what is the difference between var and let for reference types?

When you use let it is not the object that is constant but the reference to the object. That means you cannot do this:

let d = DataModel()
d = someOtherDataModel   // error: cannot change the reference

The constant d can never point to another object, but the object itself can still change.

It’s OK if you have trouble wrapping your head around this. The distinction between value types and reference types is an important idea in software development, but it’s also something which takes a while to understand.

My suggestion is that you use let whenever you can and change to var when the compiler complains. Note that optionals always need to be var, because being an optional implies that it can change its value at some point.

You can find the project files for the app up to this point under 17-Improved-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.