-1

I need to save data from a segued ViewController (“ScoreView.swift”) to “ScoreHistory.swift” using NSCoding. I tried but the data isn't showing up in "ScoreTableViewController.swift". What am I missing?

I have this ScoreView.swift which has the following code: (Pls note that this is a "segued" view where data has been passed from another ViewController)

class ScoreView: UIViewController {

var dateToday = NSDate()

var score: ScoreHistory?

var numberofquestions:String = ""
var scorepassed:String = ""
var scorepercentpassed:String = ""
var scoreremarkspassed:String = ""
var totalduration:String!
var incorrectanswerspassed:String = ""
var skippedquestionspassed:String = ""


override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.

    datePlayedLabel.text = dateToday.description       
    totalScoreLabel.text = scorepassed    
    scorePercentage.text = scorepercentpassed
    totalAnsweredLabel.text = numberofquestions
    totalDurationLabel.text = totalduration
    gameStatusLabel.text = "Exam Finished"

    // NSCoding

    if let score = score {

        datePlayedLabel.text = score.datePlayed
        totalScoreLabel.text   = score.totalScore
        totalAnsweredLabel.text   = score.totalAnswered
        totalDurationLabel.text   = score.totalDuration
        gameStatusLabel.text   = score.gameStatus
    }


}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

    if backMenu === sender {

        let datePlayed = datePlayedLabel.text ?? ""
        let totalScore = totalScoreLabel.text ?? ""
        let totalAnswered = totalAnsweredLabel.text ?? ""
        let totalDuration  = totalDurationLabel.text ?? ""
        let gameStatus = gameStatusLabel.text ?? ""

        // Set the score to be passed to ScoreTableViewController after the unwind segue.

        score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
    }

    NSKeyedArchiver.archiveRootObject(score!, toFile: ScoreHistory.ArchiveURL.path!)
}


override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(true)

    datePlayedLabel.text = dateToday.description       
    totalScoreLabel.text = scorepassed    
    scorePercentage.text = scorepercentpassed
    totalAnsweredLabel.text = numberofquestions
    totalDurationLabel.text = totalduration
    gameStatusLabel.text = "Exam Finished"
}

// Labels

}
}

I have ScoreHistory.swift, which has the following code:

class ScoreHistory: NSObject, NSCoding {

// MARK: Properties

var datePlayed: String
var totalScore: String
var totalAnswered: String
var totalDuration: String
var gameStatus: String

// MARK: Archiving Paths

static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("scores")

// MARK: Types

struct PropertyKey {
    static let datePlayedKey = "datePlayed"
    static let totalScoreKey = "totalScore"
    static let totalAnsweredKey = "totalAnswered"
    static let totalDurationKey = "totalDuration"
    static let gameStatusKey = "gameStatus"
}

// MARK: Initialization

init?(datePlayed: String, totalScore: String, totalAnswered: String, totalDuration: String, gameStatus: String) {
    // Initialize stored properties.

    self.datePlayed = datePlayed
    self.totalScore = totalScore
    self.totalAnswered = totalAnswered
    self.totalDuration = totalDuration
    self.gameStatus = gameStatus

    super.init()

}

// MARK: NSCoding

func encodeWithCoder(aCoder: NSCoder) {

    aCoder.encodeObject(datePlayed, forKey: PropertyKey.datePlayedKey)
    aCoder.encodeObject(totalScore, forKey: PropertyKey.totalScoreKey)
    aCoder.encodeObject(totalAnswered, forKey: PropertyKey.totalAnsweredKey)
    aCoder.encodeObject(totalDuration, forKey: PropertyKey.totalDurationKey)
    aCoder.encodeObject(gameStatus, forKey: PropertyKey.gameStatusKey)
}

required convenience init?(coder aDecoder: NSCoder) {

    let datePlayed = aDecoder.decodeObjectForKey(PropertyKey.datePlayedKey) as! String
    let totalScore = aDecoder.decodeObjectForKey(PropertyKey.totalScoreKey) as! String
    let totalAnswered = aDecoder.decodeObjectForKey(PropertyKey.totalAnsweredKey) as! String
    let totalDuration = aDecoder.decodeObjectForKey(PropertyKey.totalDurationKey) as! String
    let gameStatus = aDecoder.decodeObjectForKey(PropertyKey.gameStatusKey) as! String

    // Must call designated initializer.
    self.init(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
}

}

Here is the full code of ScoreTableViewController.swift:

class ScoreTableViewController: UITableViewController {
// MARK: Properties

var scores = [ScoreHistory]()

var dateToday = NSDate()

override func viewDidLoad() {
    super.viewDidLoad()

    // Load any saved scores, otherwise load sample data.
    if let savedScores = loadScores() {
        scores += savedScores
    } else {
        // Load the sample data.
        loadSampleScores()
    }
}

func loadSampleScores() {

    let score1 = ScoreHistory(datePlayed: dateToday.description, totalScore: "0", totalAnswered: "0", totalDuration: "0", gameStatus: "started")!

    scores += [score1]
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: - Table view data source

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return scores.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    // Table view cells are reused and should be dequeued using a cell identifier.

    let cellIdentifier = "ScoreHistoryTableViewCell"
    let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! ScoreHistoryTableViewCell

    // Fetches the appropriate note for the data source layout.
    let score = scores[indexPath.row]

    cell.datePlayedLabel.text = score.datePlayed
    cell.totalScoreLabel.text = score.datePlayed
    cell.totalScoreLabel.text   = score.totalScore
    cell.totalAnsweredLabel.text   = score.totalAnswered
    cell.totalDurationLabel.text   = score.totalDuration
    cell.gameStatusLabel.text   = score.gameStatus

    return cell
}

// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    // Return false if you do not want the specified item to be editable.
    return true
}


// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {

    if editingStyle == .Delete {

        // Delete the row from the data source
        scores.removeAtIndex(indexPath.row)
        saveScores()

        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    }
}

// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "ShowDetail" {
        let scoreDetailViewController = segue.destinationViewController as! ScoreViewController

        // Get the cell that generated this segue.
        if let selectedScoreCell = sender as? ScoreHistoryTableViewCell {
            let indexPath = tableView.indexPathForCell(selectedScoreCell)!
            let selectedScore = scores[indexPath.row]
            scoreDetailViewController.score = selectedScore
        }
    }
}

// MARK: NSCoding

func saveScores() {
    let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(scores, toFile: ScoreHistory.ArchiveURL.path!)
    if !isSuccessfulSave {
        print("Failed to save scores...")
    }
}

func loadScores() -> [ScoreHistory]? {
    return NSKeyedUnarchiver.unarchiveObjectWithFile(ScoreHistory.ArchiveURL.path!) as? [ScoreHistory]
}

@IBAction func unwindToScoreList(sender: UIStoryboardSegue) {
    if let sourceViewController = sender.sourceViewController as? ScoreViewController, score = sourceViewController.score {

        if let selectedIndexPath = tableView.indexPathForSelectedRow {
            // Update an existing note.
            scores[selectedIndexPath.row] = score
            tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)

            // Add a new score.

        let newIndexPath = NSIndexPath(forRow: scores.count, inSection: 0)
            scores.append(score)
            tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)

            saveScores()
        }
    }
}

}

GOAL: My goal is to record/store all session data from “ScoreView.swift” whenever a user finishes a quiz game. The "ScoreView" is shown after each quiz game, I plan to record each quiz results in "ScoreHistory.swift." How do I do it?

Juma Orido
  • 481
  • 1
  • 4
  • 11
  • In `ScoreView` you are archiving a single object, but in the other controller you are unarchiving a potential array. Please add more information what you're going to accomplish. – vadian Feb 15 '16 at 09:59
  • @vadian thanks for your answer, but how do I do it? – Juma Orido Feb 15 '16 at 10:01
  • I have no idea what`s your goal. – vadian Feb 15 '16 at 10:02
  • @vadian It's like a "Score History" view where the user will be able to see all of his quiz game results... I am struggling on how do I get the data from the "ScoreView" scene to store it into "ScoreHistory." – Juma Orido Feb 15 '16 at 10:09
  • As @vadian says, you're only writing one score history. If you want to maintain all past scores, your going to have to read and write all of them each time. This means in your segue, where it looks like you're adding one score to the history, you will have to unarchive them all, add one, and then archive them all again. – Michael Feb 15 '16 at 10:22
  • @Michael thanks for your comment, but I've no idea on how to do it :( – Juma Orido Feb 15 '16 at 10:27

2 Answers2

0

Your loadScores function is loading an archived array of scores:

func loadScores() -> [ScoreHistory]? {
    return NSKeyedUnarchiver.unarchiveObjectWithFile(ScoreHistory.ArchiveURL.path!) as? [ScoreHistory]
}

In your segue, you are only archiving a single score. You can't archive a ScoreHistory instance and expect to unarchive a ScoreHistory array. Where you currently have:

    score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)

You need to change this to:

    var scores = loadScores() ?? []
    score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
    scores.append(score)
    saveScores(scores)

Where loadScores and saveScores are the same as the code in ScoreTableViewController, although I've added the scores to save as a parameter given this code creates a local var.

UPDATE: It's late and I wasn't paying enough attention. You need to handle loadScores returning nil, and of course scores should be var not let or you won't be able to add to it. With these changes, scores should no longer be optional, so you won't need to unwrap it.

Michael
  • 8,891
  • 3
  • 29
  • 42
  • I am getting the following error in "scores.append(score)" // value of type '[ScoreHistory]?' has no member 'append' – Juma Orido Feb 15 '16 at 10:52
  • My bad - your loadScores function returns an Optional, so it needs to be unwrapped with "!". But have you considered @vadian's solution? Archive/unarchive in just one location is much better. – Michael Feb 15 '16 at 10:54
  • the score isn't showing yet in ScoreTableViewController... What am I missing? I greatly appreciate your help. – Juma Orido Feb 15 '16 at 11:03
  • only the sample score is showing.. – Juma Orido Feb 15 '16 at 11:04
  • When no scores are already saved, loadScores will return nil, which is not what you want. If it's nil, create a new empty array. I updated the answer. – Michael Feb 15 '16 at 11:43
0

The easiest solution is to save the changed values from the UITextField instances back to the score instance in ScoreView (why is score optional at all since you always pass a non-optional score instance ??) and unwind the segue. Then the array is saved in the method unwindToScoreList of ScoreTableViewController

  override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

    if backMenu === sender {
      score?.datePlayed = datePlayedLabel.text ?? ""
      score?.totalScore = totalScoreLabel.text ?? ""
      score?.totalAnswered = totalAnsweredLabel.text ?? ""
      score?.totalDuration  = totalDurationLabel.text ?? ""
      score?.gameStatus = gameStatusLabel.text ?? ""
    }
  }

No archiving in ScoreView !

vadian
  • 274,689
  • 30
  • 353
  • 361
  • thanks @vadian, I'll try this later and let you know :) – Juma Orido Feb 15 '16 at 11:05
  • I don't quite understand, I am using UILabel for scores but don't use user input to get the data.. Sorry I'm quite new to Swift – Juma Orido Feb 15 '16 at 11:21
  • If you're using just labels, why are you storing all values back and forth to variables? – vadian Feb 15 '16 at 11:26
  • The values are automatically generated from ScoreView, for instance, after the user finishes the exam, the ScoreView displays how many answers he got correctly.. I need to have these data stored in ScoreHistory.. – Juma Orido Feb 15 '16 at 11:28
  • Sorry, your code is confusing. I see in the code that you set the labels in `viewDidLoad` to the variable values (which are empty strings), then if `score` is not `nil` (it's always not `nil` though) the labels to the values of the passed score and once again in `viewWillAppear`. Then in `prepareForSegue` you store the values from the labels back to new local variables and create a new `ScoreHistory` instance. It might be easier to keep the passed `score` as model and just update the labels. Then in the unwind task pass that instance back to the source view controller and save the array. – vadian Feb 15 '16 at 11:43
  • yes, it's really confusing I'm sorry, I am much confused too :( Do you know a sample project which does this correctly? – Juma Orido Feb 15 '16 at 11:47
  • Think about the logic of the `ScoreView` class and write it yourself. It's much more fun than copy&paste snippets but having no idea what's going on. – vadian Feb 15 '16 at 11:50
  • I appreciate your suggestion, yes it's really fun, but I think I've been doing that but can't seem to find the logic.. I think a little help such as a sample code would get me started :) – Juma Orido Feb 15 '16 at 11:53
  • The `ScoreView` class seems to be incomplete (for example the declaration of the labels is missing) so I don't know exactly what it's supposed to do for example the purpose of the game status and date. – vadian Feb 15 '16 at 12:01