Update note: Ehab Amer updated this for Swift 4.2, 12 and Xcode 10. Ray Wenderlich wrote the original.

There are many ways to your to disk in iOS – raw file APIs, Property List Serialization, Core , third-party-solutions like Realm and, of course, .

Files and folders are fun to work with!

For apps with heavy data requirements, Core Data or Realm is often the best way to go. For light data requirements, NSCoding is often better because it’s easier to adopt and use.

Swift 4 also offers another lightweight alternative: Codable. This is very similar to NSCoding, but there are some differences.

If you need a simple way to persist data to disk, both NSCoding and Codable are great options! However, neither supports querying or creating complex object graphs. If you need these, you should use Core Data, Realm or another database solution instead.

The main downside to NSCoding is it requires you to depend on Foundation. Codable doesn’t depend on any frameworks, but you must write your models in Swift to use it.

Codable also offers several rich features that NSCoding doesn’t. For example, you can use it to easily serialize models into JSON.

Many Foundation and UIKit class uses NSCoding because it has been around since iOS 2.0 — yes, a decade! Codable was added in Swift 4, just about a year ago, and since many Apple classes are written in Objective-C, they likely won’t be updated anytime soon to support Codable.

Whether you choose to use NSCoding or Codable in your , it’s a good idea to understand how both work. In this tutorial, you’ll learn all about NSCoding!

Getting Started

You’ll work on a sample project called “Scary Creatures.” This app lets you save photos of creatures and rate how scary they are. However, it doesn’t persist data at this point, so if you restart the app, all of the creatures you added would be lost. Consequently, the app isn’t very useful… yet!

ScaryCreatures app: The scariest of them all!

In this tutorial, you’ll save and load the data of each creature using NSCoding and FileManager. After that, you’ll get acquainted with NSSecureCoding and what it can do to improve data loading in your apps.

To start, click Download Materials at the top or bottom of this tutorial. Open the starter project in Xcode and explore the files in the project:

The main files you’ll work on are:

  • ScaryCreatureData.swift contains the simple data on the creature, the name and rating.
  • ScaryCreatureDoc.swift contains the complete information on the creature including the data, the thumbnail image and the full image of the creature.
  • MasterViewController.swift displays the list of all stored creatures.
  • DetailViewController.swift shows the details of a selected creature and lets you rate it.

Build and run to get a feel for how the app works.

Implementing NSCoding

NSCoding is a protocol that you can implement on your data classes to support the encoding and decoding of your data into a data buffer, which can then persist on disk.

Implementing NSCoding is actually ridiculously easy — that’s why you may find it helpful to use. Watch how quickly you can bang this out!

First, open ScaryCreatureData.swift and add NSCoding to the class declaration like this:


class ScaryCreatureData: NSObject, NSCoding

Then add these two methods to the class:


func encode(with aCoder: NSCoder) {
  //add code here
}

required convenience init?(coder aDecoder: NSCoder) {
  //add code here
  self.init(title: "", rating: 0)
}

You’re required to implement these two methods to make a class conform to NSCoding. The first method encodes an object. The second one decodes the data to instantiate a new object.

In short, encode(with:) is the encoder and init(coder:) is the decoder.

Note: The presence of the keyword convenience here is not a requirement of NSCoding. It’s there because you call a designated initializer, init(title:rating:), within that initializer. If you try to remove it, Xcode will give you an error. If you choose to automatically fix it, the editor will add the same keyword again.

Before you implement these two methods, add the following enumeration in the beginning of the class for the sake of code organization:


enum Keys: String {
  case title = "Title"
  case rating = "Rating"
}

Despite seeming very trivial, it’s worth it. Codable keys use the String data type. Strings are easy to misspell without the compiler catching your mistake. By using an enumeration, the compiler will ensure that you’re always using consistent key names, and Xcode will offer you code completion as you type.

Now, you are ready for the fun part. Add the following to encode(with:):


aCoder.encode(title, forKey: Keys.title.rawValue)
aCoder.encode(rating, forKey: Keys.rating.rawValue)

encode(_:forKey:) writes the value supplied as the first parameter and binds it to a key. The value provided must be of a type that also conforms to the NSCoding protocol.

Add the following code to the beginning of init?(coder:):


let title = aDecoder.decodeObject(forKey: Keys.title.rawValue) as! String
let rating = aDecoder.decodeFloat(forKey: Keys.rating.rawValue)

This does exactly the opposite. You read the value from the NSCoder object provided from a specified key. Since you are saving two values, you want to read the same two values again to resume the app normally.

You now need to actually construct the creature’s data with what you decoded. Replace this line:


self.init(title: "", rating: 0)

with this:


self.init(title: title, rating: rating)

That’s it! With those few lines of code, the class ScaryCreatureData conforms to NSCoding.

Loading and Saving to Disk

You next need to add code to access the disk, read, and write the stored creature data.

For performance efficiency — which is nice to always keep in mind when building an app — you won’t load all the data at once.

Adding the Initializer

Open ScaryCreatureDoc.swift and add the following to the end of the class:


var docPath: URL?
  
init(docPath: URL) {
  super.init()
  self.docPath = docPath    
}

docPath will store where the ScaryCreatureData information is located on disk. The trick here is you should load the information in memory the first time you access it, not with the initialization of the object.

If you are creating a brand new creature, however, this path will be nil because a file wouldn’t be created yet for the document. You’ll add bookkeeping code next to ensure this is set whenever a new creature is created.

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

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.

Trying It Out!

Before you run the app, add this line right before the return at the end of the class property definition of privateDocsDir:


print(documentsDirectoryURL.absoluteString)

This will help you know exactly where on your computer the folder is that contains the docs when the app runs in the simulator.

Now, run the app. Copy the value from the console but skip the “file://” part. The path should start with “/Users” and end with “/PrivateDocuments.”

Open the Finder app. Navigate from the menu, Go ▸ Go to Folder and paste the path in the dialog:

Paste the path you copied from the console here.

When you open the folder, its contents should look like this:

Contents of the PrivateDocuments folder.

The items you see here are created by MasterViewController.loadCreatures(), which was implemented for you in the starter project. Each time you run the app, it will add more documents on disk… this isn’t actually correct! This happens because you aren’t reading the contents of the database from disk when the app loads. You’ll fix this in a moment but first, you need to implement a few more things.

If the user triggers a delete on the table view, you also need to delete the creature from the database. In this same file, replace the implementation of tableView(_:commit:forRowAt:) with this:


if editingStyle == .delete {
  let creatureToDelete = creatures.remove(at: indexPath.row)
  creatureToDelete.deleteDoc()
  tableView.deleteRows(at: [indexPath], with: .fade)
}

One last thing you need to consider: you finished the Add and Delete functions, but what about Edit? Don’t worry… it’s just as simple as implementing Delete.

Open DetailViewController.swift and add the following line at the end of both rateViewRatingDidChange(rateView:newRating:) and titleFieldTextChanged(_:):


detailItem?.saveData()

This simply tells the ScaryCreatureDoc object to save itself when you change its information in the user interface.

Saving and Loading Images

The last thing remaining for the creature app is saving and loading images. You won’t save them inside the list file itself; it would be much more convenient to save them as normal image files right beside the other stored data, so now you’ll write the code for that.

In ScaryCreatureDoc.swift, add the following code at the end of the class:


func saveImages() {
  // 1
  if _fullImage == nil || _thumbImage == nil { return }
  
  // 2
  do {
    try createDataPath()
  } catch {
    print("Couldn't create save Folder. " + error.localizedDescription)
    return
  }
  
  // 3
  let thumbImageURL = docPath!.appendingPathComponent(Keys.thumbImageFile.rawValue)
  let fullImageURL = docPath!.appendingPathComponent(Keys.fullImageFile.rawValue)
  
  // 4
  let thumbImageData = _thumbImage!.pngData()
  let fullImageData = _fullImage!.pngData()
  
  // 5
  try! thumbImageData!.write(to: thumbImageURL)
  try! fullImageData!.write(to: fullImageURL)
}

This is a bit similar to what you wrote before in saveData():

  1. Ensure that there are images stored; otherwise, there’s no point continuing the execution.
  2. Create the data path if needed.
  3. Build the paths that will point to each file on the disk.
  4. Convert each image to its PNG data representation to be ready for you to write on disk.
  5. Write the generated data on disk in their respective paths.

There are two points in the project where you want to call saveImages().

The first is in the initializer init(title:rating:thumbImage:fullImage:). Open ScaryCreatureDoc.swift and, at the end of this initializer, right after saveData(), add the following line:


saveImages()

The second point is in DetailViewController.swift inside imagePickerController(_:didFinishPickingMediaWithInfo:). You will find a dispatch closure wherein you update the images in detailItem. Add this line to the end of the closure:


self.detailItem?.saveImages()

Now, you can save, update and delete creatures. The app is ready to save all the scary and non-scary creatures you may come across in the future. :]

If you were to build and run now and restore your scary creatures from disk, you’d find that some have images and others do not, like this:

missing images

Using the path printed in Xcode’s debug console, find and delete the PrivateDocuments folder. Now build and run once. You’ll see the the initial creatures with their images:

saved images

While you’re saving your creatures, you can’t see what you’ve saved yet. Open MasterViewController.swift and replace the implementation of loadCreatures() with this:


creatures = ScaryCreatureDatabase.loadScaryCreatureDocs()

This loads the creatures from disk instead of the using the pre-populated list.

Build and run again. Try changing the the title and the rating. When you return to the main screen, the app saves your changes to disk.

not so scary ghost

Implementing NSSecureCoding

In iOS 6, Apple introduced something new that is built on top of NSCoding. You may have noticed that you decode values from an archive to store them in a variable like this line:


let title = aDecoder.decodeObject(forKey: Keys.title.rawValue) as! String

When reading the value, it is already loaded to memory, then you cast it to the data type you know it should be. If something went wrong and the type of the object previously written couldn’t be cast to the required data type, the object would be completely loaded in memory, then the cast attempt would fail.

The trick is the sequence of actions; although the app will not use the object at all, the object has already been loaded fully in memory, then released after the failed cast.

NSSecureCoding provides a way to load the data while validating its class as it is being decoded, instead of afterwards. And the best part is that it’s super easy to implement.

First, in ScaryCreatureData.swift, make the class implement the protocol NSSecureCoding so the class declaration looks like this:

class ScaryCreatureData: NSObject, NSCoding, NSSecureCoding

Then add the following code at the end of the class:


static var supportsSecureCoding: Bool {
  return true
}

This is all you need to comply with NSSecureCoding, but you didn’t gain the benefits from it yet.

Replace the encode(with:) implementation with this:


aCoder.encode(title as NSString, forKey: Keys.title.rawValue)
aCoder.encode(NSNumber(value: rating), forKey: Keys.rating.rawValue)

Now, replace the implementation of init?(coder:) with this:


let title = aDecoder.decodeObject(of: NSString.self, forKey: Keys.title.rawValue) 
  as String? ?? ""
let rating = aDecoder.decodeObject(of: NSNumber.self, forKey: Keys.rating.rawValue)
self.init(title: title, rating: rating?.floatValue ?? 0)

If you look at the new initializer code, you will notice that this decodeObject(of:forKey:) is different from decodeObject(forKey:) as the first parameter it takes is a class.

Unfortunately, using NSSecureCoding requires you to use the string and float counterparts in Objective-C; that’s why NSString and NSNumber are used, then the values are converted back to Swift String and Float.

The last step is to tell the NSKeyedArchiver to use secure coding. In ScaryCreatureDoc.swift, change the following line in saveData():


let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data, 
                                                  requiringSecureCoding: false)

To this instead:


let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data, 
                                                  requiringSecureCoding: true)

Here, you simply pass true to requiringSecureCoding instead of false. This tells NSKeyedArchiver to enforce NSSecureCoding for the object and its descendants when you archive it.

Note: The files previously written without NSSecureCoding will not be compatible now. You need to delete any previously saved data or uninstall the app from the simulator. In a real-world scenario, you must migrate the old data.

Where to Go From Here?

NSKeyedArchiver and NSKeyedUnarchiver classes are not the only way to encode and decode data to a be easily written on disk. There are many other formats like JSON.

The easiest way to serialize Swift models to JSON is by conforming to Codable. To learn how this works, see our tutorial about it.

I hope you enjoyed this tutorial. If you have any questions or comments, please don’t be “scared” and join the forum discussion below. :]



Source link https://www.raywenderlich.com/6733-nscoding-tutorial-for-ios-how-to--save-app-data

LEAVE A REPLY

Please enter your comment!
Please enter your name here