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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
NSCoding Tutorial for iOS: How to Permanently Save App Data
25 mins
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:
- Ensure that there is something in
data
, otherwise simply return as there is nothing to save. - Call
createDataPath()
in preparation for saving the data inside the created folder. - Build the path of the file where you will write the information.
- Encode
data
, an instance ofScaryCreatureData
, which you previously made conform toNSCoding
. You setrequiringSecureCoding
tofalse
for now, but you’ll get to this later. - 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.
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:
- If the data has already been loaded to memory, just return it.
- Otherwise, read the contents of the saved file as a type of
Data
. - 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:
- Get the app’s Documents folder, which is a standard folder that all apps have.
- Build the path pointing to the database folder that has everything stored inside.
- 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:
- Get all the contents of the database folder.
- Filter the list to only include items that end with
.scarycreature
. - 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.
OK — you’re almost done! Time to try it out.