NSCoding Tutorial for iOS: How to Permanently Save App Data

In this NSCoding tutorial, you’ll learn how to save and persist iOS app data so that your app can resume its state after quitting. By Ehab Amer.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Adding Bookkeeping Code

Add this enum to the beginning of ScaryCreatureDoc, right after the opening curly brace:

enum Keys: String {
  case dataFile = "Data.plist"
  case thumbImageFile = "thumbImage.png"
  case fullImageFile = "fullImage.png"
}

Next, replace the getter for thumbImage with:

get {
  if _thumbImage != nil { return _thumbImage }
  if docPath == nil { return nil }

  let thumbImageURL = docPath!.appendingPathComponent(Keys.thumbImageFile.rawValue)
  guard let imageData = try? Data(contentsOf: thumbImageURL) else { return nil }
  _thumbImage = UIImage(data: imageData)
  return _thumbImage
}

Next, replace the getter for fullImage with:

get {
  if _fullImage != nil { return _fullImage }
  if docPath == nil { return nil }
  
  let fullImageURL = docPath!.appendingPathComponent(Keys.fullImageFile.rawValue)
  guard let imageData = try? Data(contentsOf: fullImageURL) else { return nil }
  _fullImage = UIImage(data: imageData)
  return _fullImage
}

Since you are going to save each creature in its own folder, you’ll create a helper class to provide the next available folder to store the creature’s doc.

Create a new Swift file named ScaryCreatureDatabase.swift and add the following at the end of the file:

class ScaryCreatureDatabase: NSObject {
  class func nextScaryCreatureDocPath() -> URL? {
    return nil
  }
}

You’ll add more to this new class in a little while. For now though, return to ScaryCreatureDoc.swift and add the following to the end of the class:

func createDataPath() throws {
  guard docPath == nil else { return }

  docPath = ScaryCreatureDatabase.nextScaryCreatureDocPath()
  try FileManager.default.createDirectory(at: docPath!,
                                          withIntermediateDirectories: true,
                                          attributes: nil)
}

createDataPath() does exactly what its name says. It fills the docPath property with the next available path from the database, and it creates the folder only if the docPath is nil. If it isn’t, this means it has already correctly happened.

Saving Data

You’ll next add logic to save ScaryCreateData to disk. Add this code after the definition of createDataPath():

func saveData() {
  // 1
  guard let data = data else { return }
    
  // 2
  do {
    try createDataPath()
  } catch {
    print("Couldn't create save folder. " + error.localizedDescription)
    return
  }
    
  // 3
  let dataURL = docPath!.appendingPathComponent(Keys.dataFile.rawValue)
    
  // 4
  let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data, 
                                                    requiringSecureCoding: false)
    
  // 5
  do {
    try codedData.write(to: dataURL)
  } catch {
    print("Couldn't write to save file: " + error.localizedDescription)
  }
}

Here’s what this does:

  1. Ensure that there is something in data, otherwise simply return as there is nothing to save.
  2. Call createDataPath() in preparation for saving the data inside the created folder.
  3. Build the path of the file where you will write the information.
  4. Encode data, an instance of ScaryCreatureData, which you previously made conform to NSCoding. You set requiringSecureCoding to false for now, but you’ll get to this later.
  5. Write the encoded data to the file path created in step three.

Next, add this line to the end of init(title:rating:thumbImage:fullImage:):

saveData()

This ensures the data is saved after a new instance has been created.

Great! This takes care of saving data. Well, the app still doesn’t save images actually, but you’ll add this later in the tutorial.

Loading Data

As mentioned above, the idea is to load the information to memory when you access it for the first time and not the moment you initialize the object. This can improve the loading time of the app if you have a long list of creatures.

Note: The properties in ScaryCreatureDoc are all accessed through private properties with getters and setters. The starter project itself doesn’t benefit from that, but it’s already added to make it easier for you to proceed with the next steps.

Open ScaryCreatureDoc.swift and replace the getter for data with the following:

get {
  // 1
  if _data != nil { return _data }
  
  // 2
  let dataURL = docPath!.appendingPathComponent(Keys.dataFile.rawValue)
  guard let codedData = try? Data(contentsOf: dataURL) else { return nil }
  
  // 3
  _data = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(codedData) as?
      ScaryCreatureData
  
  return _data
}

This is all you need to load the saved ScaryCreatureData that you previously created by calling saveData(). Here’s what it does:

  1. If the data has already been loaded to memory, just return it.
  2. Otherwise, read the contents of the saved file as a type of Data.
  3. Unarchive the contents of the previously encoded ScaryCreatureData object and start using them.

You can now save and load data from disk! However, there’s a bit more to it before the app is ready to ship.

Deleting Data

The app should also allow the user to delete a creature; maybe it’s too scary to stay. :]

Add the following code right after the definition of saveData():

func deleteDoc() {
  if let docPath = docPath {
    do {
      try FileManager.default.removeItem(at: docPath)
    }catch {
      print("Error Deleting Folder. " + error.localizedDescription)
    }
  }
}

This method simply deletes the whole folder containing the file with the creature data inside it.

Completing ScaryCreatureDatabase

The class ScaryCreatureDatabase you previously created has two jobs. The first, which you already wrote an empty method for, is to provide the next available path to create a new creature folder. Its second job is to load all the stored creatures you saved previously.

Before implementing either of these two capabilities, you need a helper method that returns where the app is storing the creatures — where the database actually is.

Open ScaryCreatureDatabase.swift, and add this code right after the opening class curly brace:

static let privateDocsDir: URL = {
  // 1
  let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
  
  // 2
  let documentsDirectoryURL = paths.first!.appendingPathComponent("PrivateDocuments")
  
  // 3
  do {
    try FileManager.default.createDirectory(at: documentsDirectoryURL,
                                            withIntermediateDirectories: true,
                                            attributes: nil)
  } catch {
    print("Couldn't create directory")
  }
  return documentsDirectoryURL
}()

This is a very handy variable that stores the calculated value of the database folder path, which you here name “PrivateDocuments.” Here’s how it works:

  1. Get the app’s Documents folder, which is a standard folder that all apps have.
  2. Build the path pointing to the database folder that has everything stored inside.
  3. Create the folder if it isn’t there and return the path.

You’re now ready to implement the two functions mentioned above. You’ll start with loading the database from the saved docs. Add the following code to the bottom of the class:

class func loadScaryCreatureDocs() -> [ScaryCreatureDoc] {
  // 1
  guard let files = try? FileManager.default.contentsOfDirectory(
    at: privateDocsDir,
    includingPropertiesForKeys: nil,
    options: .skipsHiddenFiles) else { return [] }
  
  return files
    .filter { $0.pathExtension == "scarycreature" } // 2
    .map { ScaryCreatureDoc(docPath: $0) } // 3
}

This loads all the .scarycreature files stored on disk and returns an array of ScaryCreatureDoc items. Here, you do this:

  1. Get all the contents of the database folder.
  2. Filter the list to only include items that end with .scarycreature.
  3. Load the database from the filtered list and return it.

Next, you want to properly return the next available path for storing a new document. Replace the implementation of nextScaryCreatureDocPath() with this:

// 1
guard let files = try? FileManager.default.contentsOfDirectory(
  at: privateDocsDir,
  includingPropertiesForKeys: nil,
  options: .skipsHiddenFiles) else { return nil }

var maxNumber = 0

// 2
files.forEach {
  if $0.pathExtension == "scarycreature" {
    let fileName = $0.deletingPathExtension().lastPathComponent
    maxNumber = max(maxNumber, Int(fileName) ?? 0)
  }
}

// 3
return privateDocsDir.appendingPathComponent(
  "\(maxNumber + 1).scarycreature",
  isDirectory: true)

Similar to the method before it, you get all the contents of the database, filter them, append to privateDocsDir and return it.

An easy way to keep track of all the items on disk is to name the folders by numbers; by finding the folder named as the highest number, you will easily be able to provide the next available path.

Note: Using a number is just a way to name and track folders for a document-based database. You can choose an alternative way as long as each folder has a unique name so that you don’t accidentally replace an existing item with a new one.

OK — you’re almost done! Time to try it out.