Joyful Games – iPhone game development with Apple’s Swift new language

Email Contact

Play SnakeJoy! The most addicting fun snake game on the internet

Play SnakeJoy in Your Browser!

Saving SpriteKit Game Data in Swift – It’s Easy with NSCoder

Saving SpriteKit game information on iOS is a common requirement. We need to save the player’s progress, preferences, and high scores. I recently implemented a high score system for Krate. Though this Swift code is tailored for score keeping, it can be easily adapted to persist save game data or any other information you need to save between games.

We’ll inherit NSCoder to serialize our objects, then store the data using one of several iOS data persistence methods. I chose to save my high score data using plist files, saved inside my game’s directory. (You can also use NSUserDefaults to store serialized objects, though it is intended only for user preferences. Another option is Core Data, which is robust and useful for complicated Swift persistence tasks.)

First off, we’ll need a serializable class that will store our save game data. NSCoding allows us to translate our class into data that can be saved to the file system.

// inherit from NSCoding to make instances serializable
class HighScore: NSObject, NSCoding {
    let score:Int;
    let dateOfScore:NSDate;
    
    init(score:Int, dateOfScore:NSDate) {
        self.score = score;
        self.dateOfScore = dateOfScore;
    }
    
    required init(coder: NSCoder) {
        self.score = coder.decodeObjectForKey("score")! as Int;
        self.dateOfScore = coder.decodeObjectForKey("dateOfScore")! as NSDate;
        super.init()
    }
    
    func encodeWithCoder(coder: NSCoder) {
        coder.encodeObject(self.score, forKey: "score")
        coder.encodeObject(self.dateOfScore, forKey: "dateOfScore")
    }
}

Great! Next, we want to create a manager class to store an array of HighScores, and perform the save action. Mind the comments – the code required to save and read plist files is long:

class HighScoreManager {
    var scores:Array<HighScore> = [];
    
    init() {
        // load existing high scores or set up an empty array
        let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
        let documentsDirectory = paths[0] as String
        let path = documentsDirectory.stringByAppendingPathComponent("HighScores.plist")
        let fileManager = NSFileManager.defaultManager()
        
        // check if file exists
        if !fileManager.fileExistsAtPath(path) {
            // create an empty file if it doesn't exist
            if let bundle = NSBundle.mainBundle().pathForResource("DefaultFile", ofType: "plist") {
                fileManager.copyItemAtPath(bundle, toPath: path, error:nil)
            }
        }
        
        if let rawData = NSData(contentsOfFile: path) {
            // do we get serialized data back from the attempted path?
            // if so, unarchive it into an AnyObject, and then convert to an array of HighScores, if possible
            var scoreArray: AnyObject? = NSKeyedUnarchiver.unarchiveObjectWithData(rawData);
            self.scores = scoreArray as? [HighScore] ?? [];
        }
    }
    
    func save() {
        // find the save directory our app has permission to use, and save the serialized version of self.scores - the HighScores array.
        let saveData = NSKeyedArchiver.archivedDataWithRootObject(self.scores);
        let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray;
        let documentsDirectory = paths.objectAtIndex(0) as NSString;
        let path = documentsDirectory.stringByAppendingPathComponent("HighScores.plist");
        
        saveData.writeToFile(path, atomically: true);
    }
    
    // a simple function to add a new high score, to be called from your game logic
    // note that this doesn't sort or filter the scores in any way
    func addNewScore(newScore:Int) {
        let newHighScore = HighScore(score: newScore, dateOfScore: NSDate());
        self.scores.append(newHighScore);
        self.save();
    }
}

We can persist any type of game data we need with this same pattern . . . and though the code to write and read plist files is a bit verbose, the concept is actually quite straightforward. Kindly leave a comment with any questions or suggestions.

A quick note of credit: thanks to this blog post for solid plist persistence info.

  • Siarhei Brazil

    Hello. Thx, for your post. Apparently you provided the code as an example, but it would be better to place the code for formatting the name of the Highlight.plist file, into separate function, as it is accessed several times. That’s kind of premature optimization :))

    Plus, to keep the latest score ( or set of scores ) in user defaults – kind of caching logic, accessing “RAM” is better the “HDD” – flash store

  • Pingback: Battle of Brothers | A Video Game Programming Duel()

  • NSKeyedUnarchiver could throw exceptions when unarchiving objects.
    How can I avoid this when a corrupt file is being read since Swift doesn’t have exception handling?

    • Stephen Haney

      Hi CC-Dog. I don’t think you can avoid it yet with Swift. At least not until exception handling is introduced. If this is a concern, I’d suggest using an obj-c try/catch wrapper. Not the solution you were hoping for, but I’m not aware of anything else at the moment.

      • Thanks for your reply.
        I believe they don’t want exception handling in Swift since it breaks ARC, so I might have to use a objc wrapper until NSKeyedUnarchiver is updated.

  • Josh Kneedler

    thanks for this guide. just noticing the “;” in the code that aren’t needed with swift.

    • Stephen Haney

      Hi Josh, thanks for the comment. I do most of my work in C#, so the semi-colons are ingrained in my fingers 😀 But you’re right, there’s no reason to use them in Swift; I’ve stopped using them since writing this.

      • Josh Kneedler

        no problem. this example is great though. i’ve got the data saving to the plist file in my game. i’m working on the flow now of accessing the data. i want to play around with loading the plist file and also just loading the HighScore data that has been serialized. what would you say is the best approach to loading your game data once it has been saved?

        • Stephen Haney

          The init function of HighScoreManager is basically performing a load, though it also creates a new file if it doesn’t already exist. The last if block in the init function turns the file path into a Swift usable array.

          You could adapt parts of the init into a standalone load function if you don’t want to automatically pull the entire plist into memory when you instantiate the HighScoreManager. Perhaps just check if the file exists – or create it – in init, then access the file in a load function you can call when you need it.

          Good question though. I haven’t actually taken this any farther than the high score system in this post, so let me know what you discover.

          • Josh Kneedler

            i will! i’m in research mode so just having fun.

          • Josh Kneedler

            grabbing the array set by the init works great. i played around with parsing the plist file and that got a bit messy.

          • Stephen Haney

            Yeah, I would imagine that would be another layer of depth. Thanks for the update.

  • Wraithseekerr .

    Hey the code doesn’t seem to be working

    Could not cast value of type NSConcreteMutableData to NSArray

    • Stephen Haney

      Hi Wraithseeker, sorry I took so long to reply to this. I’m sure you’ve moved on to another solution now, but I just wanted to close the loose thread. I’m not sure what’s causing your error. It could be a version different in Swift, as this post is nearly a year old now (though I don’t get this error with Xcode 6.4 either). Anyway, hope you found a good solution for your need!

  • Albert Barrientos

    Error un Swift 2 🙁

  • stan

    is it possible to save subclassed objects with this method? i created weapons by subclassing sknode with custom properties…can i save the entire object and it’s current properties easily?

  • Noah Covey

    What is the “DefaultFile.plist”?

    In your code, the file you are copying to “HighScores.plist” is called “DefaultFile.plist.” However, there is no such thing as “DefaultFile.plist”. I assume this is just a placeholder, so what do I put there instead? I tried “Info.plist”, but that gives me an error.

    Thanks

    • Noah Covey

      What I don’t understand is how a new file is being created by copying “DefaultFile.plist” to “HighScores.plist”. Don’t we need to create “HighScores.plist” first?

      • Stephen Haney

        Hi Noah, I think you’re running into issues because this code was written in Swift 1. Check out this SO thread on how to convert it to Swift 2: http://stackoverflow.com/questions/33806426/error-when-trying-to-save-gamedata

        Let me know if you’re still having issues and I can help dig in in deeper.

        Best,
        Stephen

        • Noah Covey

          Thanks so much for the help! I have it all working and it is great.

          Just FYI, I’m using your code in conjunction with another tutorial: http://battleofbrothers.com/sirryan/saving-game-data-in-spritekit. This allows me to store many variables in one class, not just high scores (after a few hours of testing and googling). If I post my code online, I’ll post a link here so people can see how I to do the saving in swift 2.

          Thank you again!

    • Noah Covey

      If I create my own file called “DefaultFile.plist,” I still get an error. Should I put values into the plist file or leave it empty?