0

I am creating a simple quiz app. I am planning to show some kind of "history" where the user can see the following:

  1. Date and time of playing
  2. Score for that particular session

How do I do that?

As of the Date and Time of playing, I saw this thread on SO: How to get the current time as datetime However, how do I "RECORD" the date(s) and time(s) the user played the game?

Regarding the Score data, I am using:

NSUserDefaults.standardUserDefaults().setInteger(currentScore, forKey: "score")

However, I am only able to get the CURRENT SCORE. How do I record the score(s) the user got for EACH session on different date(s) and time(s)?

Please note that I have no problem in getting the user's CURRENT SCORE. I need help in storing or recording the user's score(s) in multiple sessions.

For instance, I wanted to display something like this:

Date: 2/7/16
Time: 7:00 AM
Score: 70/100
Community
  • 1
  • 1
Juma Orido
  • 481
  • 1
  • 4
  • 11

3 Answers3

0

To store scores for each session, you'll need some sort of data structure associated with each user. You could use a dictionary of key value pairs that associates a date with a score. That way you would have a score for each date stored for the user.

Spencer
  • 176
  • 5
0

You need to use a database to store such data with an undefined count of records. Check out the Core Data Programming Guide by Apple here

You could then create an entity named history where you store records of the user's game play history, by inserting a new object into the entity every time the user finishes a game.

When you need to show the results, you'd create an NSFetchRequest over an NSManagedObjectContext to get all the results and display/filter them as you'd like.

Hope that helps!

mohonish
  • 1,396
  • 1
  • 9
  • 21
0

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:

  1. Encoding and decoding
  2. 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

tktsubota
  • 9,371
  • 3
  • 32
  • 40
  • Hi, may you pls help me on how do I actually "use" the codes that you gave above? How do I "link" between these classes? thanks! – Juma Orido Feb 11 '16 at 10:03
  • @mjoe7 I completely re-edited my post. Maybe that will clean things up :) – tktsubota Feb 12 '16 at 01:00
  • thanks for your help. I will try it now and let you know. :) – Juma Orido Feb 12 '16 at 02:10
  • I created the SessionHistory class and I got the following 2 errors: "Type 'SessionHostory' does not conform to protocol 'NSCoding' and "Value of type 'NSCoding' has no member 'encodeObject'" – Juma Orido Feb 13 '16 at 16:08
  • @mjoe7 Sorry, I made one little mistake on my part. The parameter type is supposed to be `NSCoder` not `NSCoding`. I'll edit my post. – tktsubota Feb 13 '16 at 17:29