Saving Game Data with NSCoding in Swift

March 23rd, 2016

Filed under: Game Development, iOS Development, Mac Development | 11 comments

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.

Tags: ,


11 thoughts on “Saving Game Data with NSCoding in Swift

  1. deeds says:

    Is this written backwards, on purpose? Each thing reads like it’s the thing that follows, not the code shown. encode, in the first step, only prepares for saving, it doesn’t save. etc… right?

    • Mark Szymczyk says:

      Encode does not do the actual save. You’re right there. Encode encodes the object so it can be saved.

      I started with encoding and decoding because you can’t save and load game data until your classes are set up to encode and decode data.

  2. AD says:

    Can you provide a github repo of a working copy of this sample?

    Where does loadGame() and saveGame() functions go? In the same class as the Player class or somewhere else?

    • Mark Szymczyk says:

      AD,

      I don’t have a simple game saving example to put on GitHub. Sorry.

      Most games have a Game class as the main class in the game. The loadGame() and saveGame() functions would go there. If the only game data you’re saving is player data, you could load and save the game in the Player class. But most games need to save more data than player data. You should load and save the game in a class that has access to everything you need to save.

  3. AD says:

    I’ve built a simple outline for a game using arrays, but now I realize i should have created the save/load states first.

    I’ve been using realm, but realized that the amount of data being saved is very low and realm seems too heavy for my requirements; plus i’d have to change a lot of my code

    I think I understand what you mean by putting the loadGame() and saveGame() in another class; perhaps a game model class.

    thanks

  4. AD says:

    Lets say I want to save array of books;

    var author:Author = Author(name: "Tom Clancy", books: nil)
    let book0 = Book(title: "The Hunt for Red October", author: author)
    let book1 = Book(title: "Red Storm Rising", author: author)
    let books = [book0, book1]
    author.books.append(contentsOf: books)

    How do I save the books array?
    How do I load the books array?

    • Mark Szymczyk says:

      To save the books array you create an NSKeyedArchiver object. You call the archiver’s encode() function to save the books array. Something like the following:

      archiver.encode(books, forKey: "books")

      To load the books array create an NSKeyedUnarchiver object. Call the unarchiver’s decode() function to load the books array. Something like the following code:

      books = unarchiver.decodeObject(forKey: "books") as! [Book]

      Take a look at the Saving Data to a File and Loading Data from a File sections in the article. The code is similar. Instead of encoding and decoding a single value you’re encoding and decoding an array. Give your books array a key. The fact that you have an array doesn’t have any effect on the archiving and unarchiving.

  5. AD says:

    Thanks for the answer. I would like to use the NSCoding to save relationships between different classes, such as one to many relationships.

    I’ve been able to get a working solution on github, however I am unsure if I got it right.

    As I understand it, the parent encodes its array of children, but
    the child does not attempt to encode the parent and it required to be a weak reference so that it does not stay in retain cycle. Further, when reading it should add the children.

    Anyhow, is this the correct way to do this?

    Github:
    https://github.com/worchyld/NSCodingParentChild/blob/master/NSCodingParentChild.playground/Contents.swift

    • Mark Szymczyk says:

      I don’t see any problems with your code, but I have never tried saving parent child relationships with NSCoding so I can’t tell if what you did is the best way to save the data.

      If you are dealing with one to many relationships, you should look into Core Data. Core Data was designed to handle relationships between different classes.

  6. Dave says:

    just thinking about security of the user’s data… is there an easy way to keep this encrypted without getting the user involved in entering passwords and so forth?

    • Mark Szymczyk says:

      Dave,

      I store the data locally on the user’s device so I haven’t had to worry about encrypting data to store in the cloud.

      Sorry about the delay in my response. I wasn’t getting email notifications I normally get when someone comments on a post.

Leave a Reply to AD Cancel reply

Your email address will not be published. Required fields are marked *