2

I have a a set of objects in Swift which I am using NSCoding to save to disk so that they can be read into memory the next time the app is run. The item being saved is an array of Volume objects. Each instance of Volume has a volumeNumber Int which is set. Both the Volume and QA class inherit from NSObject and NSCoding.

If the application is run before any data is saved it runs fine, when the encoding function is run my print output confirms that volume numbers appear to be saved appropriately (as 1 and 2 in my example). However when I then reload the application it quits with the error: fatal error: unexpectedly found nil while unwrapping an Optional value and highlights the: let volumeNumber = aDecoder.decodeObject(forKey: "volumeNumber") as! Int

To me it looks like the application is saving the Ints but for some reason is unable to find them when the application tries to read from memory. Can anyone see what I might have done wrong?

This is my Volume class:

import Foundation
class Volume: NSObject, NSCoding {
    let volumeNumber: Int
    var completed: Bool
    var questionsData: [QA]

    init (volumeNumber: Int, completed: Bool, questionsData: [QA]) {
        self.volumeNumber = volumeNumber
        self.completed = completed
        self.questionsData = questionsData
    }

    func percent() -> Int {
        var correctAnswersTotal = 0
        var percentage = 0
        for question in questionsData {
            if question.correctAnswer == question.selectedAnswer {
                correctAnswersTotal += 1
            }
        }
        percentage = ((correctAnswersTotal / questionsData.count) * 100) as Int
        print("percentage is: \(percentage)")
        return percentage
    }

    // MARK: NSCoding
    public convenience required init?(coder aDecoder: NSCoder) {
        print("Trying to read volumeNumber in decoder now")
        let volumeNumber = aDecoder.decodeObject(forKey: "volumeNumber") as! Int
        let completed = aDecoder.decodeObject(forKey: "completed") as! Bool
        let questionsData = aDecoder.decodeObject(forKey: "questionsData") as! [QA]

        self.init(volumeNumber: volumeNumber, completed: completed, questionsData: questionsData)
    }

    func encode(with aCoder: NSCoder) {
        print("about to encode \(volumeNumber) in func encode")
        aCoder.encode(volumeNumber, forKey: "volumeNumber")
        aCoder.encode(completed, forKey: "completed")
        aCoder.encode(questionsData, forKey: "questionsData")
    }
}

And this is my view controller which attempts to save/load the data: import UIKit

class VolumeTableViewController: UITableViewController {

    static var volumesArray: [Volume] = [Volume(volumeNumber: 1, completed: false, questionsData: [QA(questionsText: "Question 1", answerText: ["Answer A", "Answer B", "Answer C", "Answer D"], correctAnswer: [true, false, false, false], selectedAnswer: [false, false, false, false])])]

    override func viewDidLoad() {
        print("view did load")
        super.viewDidLoad()
        if let loadedVolumes = VolumeTableViewController.read() {
            print("Using loaded volumes...")
            VolumeTableViewController.volumesArray = loadedVolumes
        } else {
            print("Setting up fresh set of volumes...")
            VolumeTableViewController.volumesArray = [Volume(volumeNumber: 1, completed: false, questionsData: [QA(questionsText: "Vol1 Question 1", answerText: ["Answer A", "Answer B", "Answer C", "Answer D"], correctAnswer: [true, false, false, false], selectedAnswer: [false, false, false, false])]),
            Volume(volumeNumber: 2, completed: false, questionsData: [QA(questionsText: "Vol 2 Question 1", answerText: ["Answer A", "Answer B", "Answer C", "Answer D"], correctAnswer: [true, false, false, false], selectedAnswer: [false, false, false, false])])]
        }
        // Uncomment the following line to preserve selection between presentations
        // self.clearsSelectionOnViewWillAppear = false

        // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
        // self.navigationItem.rightBarButtonItem = self.editButtonItem()
    }

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

    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

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


    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "VolumeCell", for: indexPath)
        // Configure the cell...
        if VolumeTableViewController.volumesArray[indexPath.row].completed {
            let percent = VolumeTableViewController.volumesArray[indexPath.row].percent()
            cell.textLabel?.text = "Volume \(indexPath.row + 1) \(percent) %"
        }
                cell.textLabel?.text = "Volume \(indexPath.row + 1)"
        return cell
    }


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

    // MARK: - Navigation

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "goToQuestionsTableView" {
            if let indexPath = self.tableView.indexPathForSelectedRow {
                print("index selected for path for volume is: \(indexPath.row)")
                let controller = segue.destination as! QuestionsTableViewController
                controller.volume = indexPath.row
            }
        }
    }
    // Mark NSCODING
    static func save() {
        print("About to save...")
        let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
        let ArchiveURL = DocumentsDirectory.appendingPathComponent("volumesData")
        NSKeyedArchiver.archiveRootObject(VolumeTableViewController.volumesArray, toFile: ArchiveURL.path)
    }

    static  func read() -> [Volume]? {
        print("About to read()")
        let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
        let ArchiveURL = DocumentsDirectory.appendingPathComponent("volumesData")
        return NSKeyedUnarchiver.unarchiveObject(withFile: ArchiveURL.path) as? [Volume]

    }
}
laurie
  • 965
  • 1
  • 11
  • 21
  • 2
    You need to use `decodeInteger(forKey:)` and it will return 0 instead of nil in case the key doesn't exist – Leo Dabus Sep 24 '17 at 19:32
  • In case you still run into any issue you will probably need to save your QA object as data. In such case this answer will help you fix it https://stackoverflow.com/questions/33192675/using-nsuserdefaults-on-arrays/33194374#33194374 – Leo Dabus Sep 24 '17 at 19:37
  • Thanks for the suggestion however that doesn't seem to be the issue - by changing that the app fails on the next line that it's trying to decode for the same reason. The Int being saved 'shouldn't' ever be nil so I'm not sure why it is finding that. – laurie Sep 24 '17 at 19:41
  • 1
    just use `decodeBool(forKey:)` https://developer.apple.com/documentation/foundation/nscoder/1409293-decodebool – Leo Dabus Sep 24 '17 at 19:42

1 Answers1

1

In case anyone else comes across the same problem the issue was in how I was encoding my data. In particular not encoding my Int as using encodeCInt etc see code changes which solved this issue:

// MARK: NSCoding
public convenience required init?(coder aDecoder: NSCoder) {
    print("Trying to read volumeNumber in decoder now")
    let volumeNumber = aDecoder.decodeInteger(forKey: "volumeNumber")
    let completed = aDecoder.decodeBool(forKey: "completed")
  let questionsData = aDecoder.decodeObject(forKey: "questionsData") as! [QA]

    self.init(volumeNumber: volumeNumber, completed: completed, questionsData: questionsData)
}

func encode(with aCoder: NSCoder) {
    print("about to encode \(volumeNumber) in func encode")
    aCoder.encodeCInt(Int32(volumeNumber), forKey: "volumeNumber")
    aCoder.encode(completed, forKey: "completed")
    aCoder.encode(questionsData, forKey: "questionsData")
}
laurie
  • 965
  • 1
  • 11
  • 21
  • 1
    As `volumeNumber` is a regular `Int` there is no need to convert the `Int` to `Int32`, just write `aCoder.encode(volumeNumber, forKey....` – vadian Sep 25 '17 at 17:19