NSUserDefaults
probably isn't right for what you are trying to do. I recommend using NSCoding
for simple data storing. Core Data may be too complicated for something this simple. However, if you plan on saving a large data model with relationships, Core Data is the way to go.
NSCoding
NSCoding
has two parts:
- Encoding and decoding
- Archiving and unarchiving
NSHipster explains this perfectly:
NSCoding is a simple protocol, with two methods: -initWithCoder: and encodeWithCoder:. Classes that conform to NSCoding can be serialized and deserialized into data that can be either be archived to disk or distributed across a network.
That archiving is performed by NSKeyedArchiver
and NSKeyedUnarchiver
.
Session
Even without NSCoding
, it is suggested to represent data with objects. In this case, we can use the very creative name Session
to represent a session in the history.
class Session: NSObject, NSCoding {
let date: NSDate // stores both date and time
let score: Int
init(date: NSDate, score: Int) { // initialize a NEW session
self.date = date
self.score = score
super.init()
}
required init?(coder aDecoder: NSCoder) { // decodes an EXISTING session
if let decodedDate = aDecoder.decodeObjectForKey("date") as? NSDate {
self.date = decodedDate
} else {
self.date = NSDate() // placeholder // this case shouldn't happen, but clearing compiler errors
}
self.score = aDecoder.decodeIntegerForKey("score")
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(date, forKey: "date")
aCoder.encodeInteger(score, forKey: "score")
}
}
The above code in English, in order from top to bottom:
- Defining the class, conforming to
NSCoding
- The properties of a session: the date (+ time) and the score
- The initializer for a new session - simply takes a date and score and creates an session for it
- The required initializer for an existing session - decodes the date and score that is saved
decodeObjectForKey:
simply does what it says (decodes an object using a key), and it returns AnyObject?
decodeIntegerForKey:
, however, returns Int
. If none exists on file, it returns 0, which is why it isn't optional. This is the case for most of the decoding methods except for decodeObjectForKey:
- The required method for encoding an existing session - encodes the date and score
- The encoding methods are just as straightforward as the decoding methods.
That takes care of the Session
class, with the properties ready for NSCoding
. Of course, you could always add more properties and methods.
SessionHistory
While the sessions itself are nice, an object to manage the array of sessions is needed, and it also needs to conform to NSCoding
. You could also add this code to an existing class.
class SessionHistory: NSObject, NSCoding {
var sessions = [Session]()
required init?(coder aDecoder: NSCoder) {
if let decodedSessions = aDecoder.decodeObjectForKey("sessions") as? [Session] {
self.sessions = decodedSessions
} else {
self.sessions = [] // another compiler error clearer
}
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(sessions, forKey: "sessions")
}
override init() { // Used for convenience
super.init()
}
}
English translation:
- Defining the manager, conforming to
NSCoding
- Add property for the array of sessions
- Next two
NSCoding
methods do nearly the same thing as Session
. Except this time, it is with an array.
- Initializer for a new manager, which will be used below.
NSCoding
looks at this manager class and sees that it needs to encode an array of sessions, so then NSCoding
looks at the Session
class to see what to encode for those sessions.
NSKeyedArchiver/NSKeyedUnarchiver and Singletons
While all the NSCoding
is set up now, the final step is to incorporate NSKeyedArchiver
and NSKeyedUnarchiver
to actually save and load the data.
The two important methods are NSKeyedArchiver.archiveRootObject(_, toFile:)
and NSKeyedUnarchiver.unarchiveRootObjectWithFile:
Note that both methods need a file. It automagically creates the file for you, but you need to set a location. Add this to SessionHistory
:
static var dataPath: String {
let URLs = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let URL = URLs[0]
return URL.URLByAppendingPathComponent("savehistory").path! // Put anything you want for that string
}
That simply finds a location for the file. You could, of course, find somewhere else to put the file.
With the data path ready, you can use the two methods I mentioned earlier. I like to use a modified version of a singleton for the manager class to make sure I'm using the same array of objects. In the SessionHistory
class:
private static var history: SessionHistory!
static func appHistory() -> SessionHistory {
if history == nil {
if let data = NSKeyedUnarchiver.unarchiveObjectWithFile(dataPath) as? SessionHistory {
history = data
} else {
history = SessionHistory()
}
}
return history
}
This creates a private static property to store the one session history of the app. The static method checks if the session history is nil
. If so, it returns the current history on file and loads the file into the history property. Otherwise, it creates a new empty session history. After that, or if the history property already stores something, it returns the history property.
Usage
All the setup for NSCoding
and NSKeyedArchiver
is done. But how do you use this code?
Each time you want to access the session history, call
SessionHistory.appHistory()
Wherever you want to save the session history, call
NSKeyedArchiver.archiveRootObject(SessionHistory.appHistory(), toFile: SessionHistory.dataPath)
Sample usage would work like this:
let session = Session(date: someRandomDate, score: someRandomScore)
SessionHistory.appHistory().sessions.append(session)
NSKeyedArchiver.archiveRootObject(SessionHistory.appHistory(), toFile: SessionHistory.dataPath)
The session history will automatically be loaded from the file when accessed via SessionHistory.appHistory()
.
You don't really need to "link" the classes per se, you just need to append the sessions to the sessions
array of the session history.
Further Reading