Skip to main content

Saving Game Data with NSCoding in Swift

·6 mins

I reached a point in developing LetterMiner where I wanted to save the game when the player quit so they could pick up where they left off. I didn’t find a lot of information online on saving data in SpriteKit games so I’ve decided to share what I’ve learned. I hope it helps other people.

Classes #

To save data with NSCoding in Swift, that data must be in Swift classes. Swift structs don’t conform to the NSCoding protocol. If you want to use Swift structs in your game, create a SaveGame class with properties for each piece of data you want to save and do all your data reading and writing in that class.

The following example shows a simple Player class that conforms to the NSCoding protocol:

class Player: NSObject, NSCoding {
    var name = "Player 1"
    var score = 0
}

You noticed my Player class also inherits from NSObject. NSObject is the base class for the Cocoa and Cocoa Touch frameworks. If you have a class that is going to work with Apple’s frameworks, it’s usually good to inherit from NSObject.

Writing Data #

To write data the class must implement the method encodeWithCoder. Call the NSCoder method encodeObject to save a piece of data. The following code shows an example of writing data for the Player class:

func encodeWithCoder(coder: NSCoder) {
    coder.encodeObject(name, forKey: "name")
    coder.encodeObject(score, forKey: "score")
}

Swift 3 Code

func encode(with coder: NSCoder) {
    coder.encode(name, forKey: "name")
    coder.encode(score, forKey: "score")
}

Make sure you remember the key you use when writing the data. You must use the same key to read the data.

I’ve kept the example simple with one string property and one integer property. You can also write structs, classes, and arrays with the encodeObject method. NSCoder also has methods to write primitive data types like integers and floating-point numbers. What I like about encodeObject is it works on any data type.

Reading Data #

To read data the class must implement the initWithCoder method. Call the NSCoder method decodeObjectForKey to read the data. The following code shows an example of reading data for the Player class:

required convenience init(coder decoder: NSCoder) {
    self.init()
    name = decoder.decodeObjectForKey("name") as! String
    score = decoder.decodeObjectForKey("score") as! Int
}

Swift 3 Code

required convenience init(coder decoder: NSCoder) {
    self.init()
    name = decoder.decodeObject(forKey: "name") as! String
    score = decoder.decodeInteger(forKey: "score")
}

In Swift the initWithCoder method is an init method that takes a coder as an argument.

Saving Data to a File #

Saving the data to a file requires the following steps:

  1. Create an instance of NSMutableData.
  2. Create an instance of NSKeyedArchiver.
  3. Call the NSKeyedArchiver method encodeObject to save each piece of data you want to save.
  4. Call the NSKeyedArchiver method finishEncoding when you’re finished.
  5. Call the NSMutableData method writeToURL to save the data to a file.

The following code shows an example of saving a game:

var player: Player

func saveGame(notification: NSNotification) {
    let saveData = NSMutableData()
    let archiver = NSKeyedArchiver(forWritingWithMutableData: saveData)

    archiver.encodeObject(player.name, forKey: "name")
    archiver.encodeObject(player.score, forKey: "score")
    archiver.finishEncoding()

    let saveLocation = saveFileLocation()
    _ = saveData.writeToURL(saveLocation, atomically: true)
}

Swift 3 Code

func saveGame(notification: NSNotification) {
    let saveData = NSMutableData()
    let archiver = NSKeyedArchiver(forWritingWith: saveData)

    archiver.encode(player.name, forKey: "name")
    archiver.encode(player.score, forKey: "score")
    archiver.finishEncoding()

    let saveLocation = saveFileLocation()
    _ = saveData.write(to: saveLocation, atomically: true)
}

Your saveGame method doesn’t have to take a notification argument. I use the argument because I want my game to save when the player quits the game. The following code lets you register for the notification when the player quits a Mac game:

NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveGame:", 
    name:NSApplicationWillTerminateNotification, object:nil)

Swift 3 Code

NotificationCenter.default.addObserver(self, 
    selector:#selector(GameScene.saveGame(_:)), 
    name:NSNotification.Name.NSApplicationWillTerminateNotification, object:nil)

The Swift 3 code assumes the saveGame method is in a GameScene class. If your saveGame method is in another class, substitute the name of that class.

For an iOS game the notification to register for is UIApplicationDidEnterBackgroundNotification.

Notice the colon at the end of the saveGame: selector. Don’t forget to add the colon, or you’ll spend a lot of time figuring out why the notification never fires.

I will talk about the saveLocation variable later in the article.

Loading Data from a File #

Loading data from a file requires the following steps:

  1. Create an instance of NSData.
  2. Create an instance of NSKeyedUnarchiver.
  3. Call the NSKeyedUnarchiver method decodeObjectForKey to load each piece of data.
  4. Call the NSKeyedUnarchiver method finishDecoding when you’re finished.

The following example shows an example of loading a saved game:

var player = Player()

func loadGame() {
    let saveLocation = saveFileLocation()
    if let saveData = NSData(contentsOfURL: saveLocation) {
        let unarchiver = NSKeyedUnarchiver(forReadingWithData: saveData)
        player.name = unarchiver.decodeObjectForKey("name") as! String
        player.score = unarchiver.decodeObjectForKey("score") as! Int
        unarchiver.finishDecoding()
    }        
}

Swift 3 Code

func loadGame() {
    let saveLocation = saveFileLocation()
    if let saveData = try? Data(contentsOf: saveLocation) {
        let unarchiver = NSKeyedUnarchiver(forReadingWith: saveData)
        player.name = unarchiver.decodeObject(forKey: "name") as! String
        player.score = unarchiver.decodeInteger(forKey: "score")
        unarchiver.finishDecoding()
    }        
}

Where to Save the Game #

Mac games should place saved game files in the Application Support folder. You will have to create a folder with the name of your game in the Application Support folder and place the saved game files in the folder you create. You can also place iOS saved games files in the Application Support folder, but you can place them in the Documents folder if you want.

The NSFileManager class has methods to find and create directories. Call the URLForDirectory method to find the Application Support folder. The following code finds the user’s Application Support directory:

do {
    let gameSaveDirectory = try fileManager.URLForDirectory(
        .ApplicationSupportDirectory, inDomain: .UserDomainMask, 
        appropriateForURL: nil, create: true)
} catch {
    // Handle any errors here
}

Swift 3 Code

do {
    let gameSaveDirectory = try fileManager.url(
        for: .ApplicationSupportDirectory, in: .UserDomainMask, 
        appropriateFor: nil, create: true)
} catch {
    // Handle any errors here
}

The URLForDirectory function throws an error in Swift 2. That’s why the code has to be wrapped in a do block.

At this point you have access to the Application Support directory. The first time you save you must create a directory for your game inside the Application Support folder. Call the createDirectoryAtURL method to create a directory.

let myGamePath = gameSaveDirectory.URLByAppendingPathComponent("MyGame")
do {
    // Create a directory to save games the first time we save.
    try fileManager.createDirectoryAtURL(myGamePath, 
        withIntermediateDirectories: true, attributes: nil)
} catch {
    // Handle any errors here     
}

Swift 3 Code

let myGamePath = gameSaveDirectory.appendingPathComponent("MyGame")
do {
    // Create a directory to save games the first time we save.
    try fileManager.createDirectory(at: myGamePath, 
        withIntermediateDirectories: true, attributes: nil)    

} catch {
    // Handle any errors here     
}

Now you’re in your game’s Application Support folder. The final step is to create a file name for the saved game. The NSURL method URLByAppendingPathComponent lets you add the file name to the path you’ve built.

let savePath = myGamePath.URLByAppendingPathComponent("MySavedGame.savegame")

Swift 3 Code

let savePath = myGamePath.appendingPathComponent("MySavedGame.savegame")

Now you have a URL you can use to save games and load saved games. In my saveGame and loadGame functions I referred to a saveLocation variable that was the result of calling a function called saveFileLocation. The savePath variable is what saveFileLocation returns.