Home iOS & Swift Books UIKit Apprentice

18
User Defaults Written by Matthijs Hollemans & Fahim Farook

You now have an app that lets you create lists and add to-do items to those lists. All of this data is saved to long-term storage so that even if the app gets terminated, nothing is lost.

There are some improvements — both to the user interface and to the code — that you can make though.

This chapter covers the following:

  • Remember the last open list: Improve the user-experience by remembering the last open list on app re-launch.
  • Defensive programming: Adding in checks to guard against possible crashes — coding defensively instead of reacting to crashes later.
  • The first-run experience: Improving the first-run experience for the user so that the app looks more polished and user-friendly.

Remember the last open list

Imagine the user is on the Birthdays checklist and switches to another app. The Checklists app is now suspended. It is possible that at some point the app gets terminated and is removed from memory. When the user reopens the app some time later, it no longer is on Birthdays but on the main screen. Because it was terminated, the app didn’t simply resume where it left off, but got launched anew.

You might be able to get away with this, as apps don’t get terminated often unless, for example, your users play a lot of games that eat up memory, but little things like this matter in iOS apps.

Fortunately, it’s fairly easy to remember whether the user had opened a checklist and to switch to it when the app starts up.

Use UserDefaults

You could store this information in the Checklists.plist file, but for simple settings such as this, there is another option — the UserDefaults object.

A dictionary is a collection of key-value pairs
O cuyheewiby in e rindukpiow oj kag-gameu yoivm

override func tableView(
  _tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  // add this line:
  UserDefaults.standard.set(
    indexPath.row, 
    forKey: "ChecklistIndex")
  . . .
}

Navigation controller delegate

To be notified when the user presses the back button on the navigation bar, you have to become a delegate of the navigation controller. Being the delegate means that the navigation controller tells you when it pushes or pops view controllers on the navigation stack.

class AllListsViewController: UITableViewController, ListDetailViewControllerDelegate, UINavigationControllerDelegate {
// MARK: - Navigation Controller Delegates
func navigationController(
  _ navigationController: UINavigationController, 
  willShow viewController: UIViewController, 
  animated: Bool
) {
  // Was the back button tapped?
  if viewController === self {
    UserDefaults.standard.set(-1, forKey: "ChecklistIndex")
  }
}

Equal or identical

To determine whether the AllListsViewController is the newly activated view controller, you wrote:

if viewController === self {
if segue.identifier == "AddItem" {
if viewController == self

Show the last open list

The only thing that remains is to check at startup which checklist you need to show and then perform the segue to the to-do item list manually. You’ll do that in viewDidAppear().

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)

  navigationController?.delegate = self

  let index = UserDefaults.standard.integer(
    forKey: "ChecklistIndex")
  if index != -1 {
    let checklist = dataModel.lists[index]
    performSegue(
      withIdentifier: "ShowChecklist", 
      sender: checklist)
  }
}

Defensive programming

➤ Now do the following: stop the app and delete it from the Simulator by holding down on the app icon until it starts to wiggle and then deleting it. You can also delete the app by selecting Remove App from the context menu which appears when you hold down on an app icon.

Fatal error: Index out of range
let checklist = dataModel.lists[index]

Set a default value for a UserDefaults key

Fortunately, UserDefaults will let you set default values for the default values. Yep, you read that correctly. Let’s do that in the DataModel object.

func registerDefaults() {
  let dictionary = [ "ChecklistIndex": -1 ]
  UserDefaults.standard.register(defaults: dictionary)
}
[ key1: value1, key2: value2, . . . ]
[ value1, value2, value3, . . . ]
init() {
  loadChecklists()
  registerDefaults()
}

Clean up the code

In fact, let’s move all of the UserDefaults stuff into DataModel.

var indexOfSelectedChecklist: Int {
  get {
    return UserDefaults.standard.integer(
      forKey: "ChecklistIndex")
  }
  set {
    UserDefaults.standard.set(
      newValue, 
      forKey: "ChecklistIndex")
  }
}
override func viewDidAppear(_animated: Bool) {
  ...
  let index = dataModel.indexOfSelectedChecklist // change this
  if index != -1 {
  ...
  }
}
override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  // change this line
  dataModel.indexOfSelectedChecklist = indexPath.row  
  ...
}
func navigationController(
  _ navigationController: UINavigationController, 
  willShow viewController: UIViewController, 
  animated: Bool
) {
  if viewController === self {
    dataModel.indexOfSelectedChecklist = -1   // change this
  }
}

A subtle bug

It’s pretty nice that the app now remembers what screen you were on, but this new feature has also introduced a subtle bug in the app. Here’s how to reproduce it:

Fatal error: Index out of range
if index >= 0 && index < dataModel.lists.count {
if something && somethingElse {
  // do stuff
}

The first-run experience

Let’s use UserDefaults for something else. It would be nice if the first time you ran the app it created a default checklist for you, simply named “List”, and switched over to that list. This enables you to start adding to-do items right away.

Check for first run

To implement the above feature, you need to keep track in UserDefaults whether this is the first time the user runs the app. If it is, you create a new Checklist object.

func registerDefaults() {
  let dictionary = [ 
    "ChecklistIndex": -1, 
    "FirstTime": true 
  ] as [String: Any]
  UserDefaults.standard.register(defaults: dictionary)
}
func handleFirstTime() {
  let userDefaults = UserDefaults.standard
  let firstTime = userDefaults.bool(forKey: "FirstTime")

  if firstTime {
    let checklist = Checklist(name: "List")
    lists.append(checklist)

    indexOfSelectedChecklist = 0
    userDefaults.set(false, forKey: "FirstTime")
  }
}
init() {
  loadChecklists()
  registerDefaults()
  handleFirstTime()    // Add this
}

Organizing Source Files

At this point, your Project navigator probably lists your files something like this:

Project navigator file listing
Wvavivw huzizoxam rofa xaswurx

Context menu for folder
Vibxepm podo yok hagquk

Sorted file listing
Jenqel qiga daksehr

Filter file list by name
Toqseb yama sihl fy rudo

Organized file listing
Eskizufis luli jilfedn

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.