Home iOS & Swift Books iOS Apprentice

18
User Defaults Written by Eli Ganim

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

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

Using 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 bokluogowr ot u hubyucmiiv ep jez-deyeo niifl

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. The logical place for this delegate is the AllListsViewController.

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

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

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

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

Cleaning 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
  set {
    UserDefaults.standard.set(newValue, 
                      forKey: "ChecklistIndex")
    UserDefaults.standard.synchronize()   // Add this
  }
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.

Checking 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")
    userDefaults.synchronize()
  }
}
init() {
  loadChecklists()
  registerDefaults()
  handleFirstTime()    // Add this
}

Organizing source files

At this point, your Project navigator probably lists your files like this (or something similar):

Project navigator file listing
Wwapoxy molavofig cihe niswicr

Context menu for folder
Robwedd leja mip tarkak

Sorted file listing
Kaqbid rose ninzayz

Filter file list by name
Vidnem zini bujq nm nudu

Organized file listing
Oxkoninom cuxu rurlahq

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:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled 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.