Day 48 - 100 Days of Swift

3 minute read

Project 12 (part 1)

Day 48 is the first part of the twelfth project. It is another technique project and the technique this time is saving data to the disk. The API you look at for saving data today is UserDefaults. You conform the Person class to NSCoding, and then use NSKeyedArchiver and NSKeyedUnarchiver as the middle man to get from a state that can be saved in UserDefaults and one that can be used by the app. Finally, you save and load the data in places that are appropriate.

First, you make Person conform to NSCoding:

1
2
3
4
5
6
7
8
9
10
11
12
class Person: NSObject, NSCoding {

//... at the bottom of the class
required init?(coder aDecoder: NSCoder) {
    name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
    image = aDecoder.decodeObject(forKey: "image") as? String ?? ""
}

func encode(with aCoder: NSCoder) {
    aCoder.encode(name, forKey: "name")
    aCoder.encode(image, forKey: "image")
}

Then you add a method in ViewController to get the people array into a savable format, and save it:

1
2
3
4
5
6
7
8
private func save() {
    if let saveData =
        try? NSKeyedArchiver.archivedData(withRootObject: people,
                                          requiringSecureCoding: false) {
        let defaults = UserDefaults.standard
        defaults.set(saveData, forKey: "people")
    }
}

And a corresponding method to get the data from the disk and back into a format that the app can use:

1
2
3
4
5
6
7
8
private func loadPeople() {
    let defaults = UserDefaults.standard
    guard let savedPeople = defaults.object(forKey: "people") as? Data else { return }
    guard let decodedPeople =
        try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(savedPeople) as? [Person] else { return }

    people = decodedPeople
}

Then all you need to do is sprinkle those methods in wherever they make sense. For saving, that means any time after you change the data and for loading that means when this view loads:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// At the end of imagePickerController(_:didFinishPickingMediaWithInfo:)
people.append(person)
collectionView.reloadData()
save()

// In the deleteAction closure of presentCellOptionAlert
selfVC.people.remove(at: indexPath.row)
selfVC.collectionView.deleteItems(at: [indexPath])
selfVC.save()

// In the saveAction closure of renamePerson
person.name = newName
self?.collectionView.reloadItems(at: [indexPath])
self?.save()

// In viewDidLoad
loadPeople()

With that, the app works. It looks exactly the same as it did before, but now it actually saves the people that you add to the app:

Screenshot of working app.

You can find my version of this project at the end of day 48 on Github here.

Reflections

Adding persistence to the app is another big step towards making something actually useful. This is obviously a very simple implementation, and not really how we should be storing the app’s main data, but it works (assuming a relatively small number of people) so that is pretty cool. It was also a good introduction to NSCoding and NSKeyedArchiver, which I’ve never really used before.