NSKeyedUnarchiver Returns Empty

I am building an app which involves saving schedules and displaying them in a table view. I am using NSCoding to save an array of custom Schedule objects. However, when I reload the app after entering a schedule, the table view is empty again. NSKeyedArchiver and NSKeyedUnarchiver both run successfully, but I am only getting an empty array after restarting the app. What am I doing wrong?


Here is the Schedule class:

import Foundation
class Schedule: NSObject, NSCoding {
  
    var name: String
    var division: Bool
  
    var carrierA: String
    var carrierB: String
    var carrierC: String
    var carrierD: String
    var carrierE: String
    var carrierF: String
    var carrierG: String
  
    override var description: String {
        return "\(name)'s schedule is: \(carrierA), \(carrierB), \(carrierC), \(carrierD), \(carrierE), \(carrierF), \(carrierG)"
    }
  
    struct PropertyKey {
        static let name = "name"
        static let division = "division"
        static let carrierA = "carrierA"
        static let carrierB = "carrierB"
        static let carrierC = "carrierC"
        static let carrierD = "carrierD"
        static let carrierE = "carrierE"
        static let carrierF = "carrierF"
        static let carrierG = "carrierG"
    }
  
    static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    static let archiveURL = documentsDirectory.appendingPathComponent("schedules")
  
    init(name: String, division: Bool, carrierA: String, carrierB: String, carrierC: String, carrierD: String, carrierE: String, carrierF: String, carrierG: String) {
        self.name = name
        self.division = division
      
        self.carrierA = carrierA
        self.carrierB = carrierB
        self.carrierC = carrierC
        self.carrierD = carrierD
        self.carrierE = carrierE
        self.carrierF = carrierF
        self.carrierG = carrierG
    }
  
    static func loadFromFile() -> [Schedule]? {
        return NSKeyedUnarchiver.unarchiveObject(withFile: Schedule.archiveURL.path) as? [Schedule]
    }
  
    static func saveToFile(schedules: [Schedule]) {
        NSKeyedArchiver.archiveRootObject(schedules, toFile: Schedule.archiveURL.path)
    }
  
    required convenience init?(coder aDecoder: NSCoder) {
        guard let name = aDecoder.decodeObject(forKey: PropertyKey.name) as? String,
            let division = aDecoder.decodeObject(forKey: PropertyKey.division) as? Bool,
            let carrierA = aDecoder.decodeObject(forKey: PropertyKey.carrierA) as? String,
            let carrierB = aDecoder.decodeObject(forKey: PropertyKey.carrierB) as? String,
            let carrierC = aDecoder.decodeObject(forKey: PropertyKey.carrierC) as? String,
            let carrierD = aDecoder.decodeObject(forKey: PropertyKey.carrierD) as? String,
            let carrierE = aDecoder.decodeObject(forKey: PropertyKey.carrierE) as? String,
            let carrierF = aDecoder.decodeObject(forKey: PropertyKey.carrierF) as? String,
            let carrierG = aDecoder.decodeObject(forKey: PropertyKey.carrierG) as? String else { return nil }
        self.init(name: name, division: division, carrierA: carrierA, carrierB: carrierB, carrierC: carrierC, carrierD: carrierD, carrierE: carrierE, carrierF: carrierF, carrierG: carrierG)
    }
  
    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: PropertyKey.name)
        aCoder.encode(division, forKey: PropertyKey.division)
        aCoder.encode(carrierA, forKey: PropertyKey.carrierA)
        aCoder.encode(carrierB, forKey: PropertyKey.carrierB)
        aCoder.encode(carrierC, forKey: PropertyKey.carrierC)
        aCoder.encode(carrierD, forKey: PropertyKey.carrierD)
        aCoder.encode(carrierE, forKey: PropertyKey.carrierE)
        aCoder.encode(carrierF, forKey: PropertyKey.carrierF)
        aCoder.encode(carrierG, forKey: PropertyKey.carrierG)
    }
}


And here is the TableViewController:

import UIKit
class SettingsTableViewController: UITableViewController {
   
    var schedules = [Schedule]()
   
    var previousIndex: Int?
   
    @objc func refreshControlActivated(sender: UIRefreshControl) {
        tableView.reloadData()
        sender.endRefreshing()
    }
   
    /
   
    override func viewDidLoad() {
        super.viewDidLoad()
        /
        /
        self.navigationItem.title = "Manage Schedules"
        if #available(iOS 11, *) {
            navigationController?.navigationBar.prefersLargeTitles = true
        }
       
        if let savedSchedules = Schedule.loadFromFile() {
            print(savedSchedules)
            schedules = savedSchedules
            tableView.reloadData()
            print("Fetched fresh, new schedules!")
            print(schedules)
        }
       
        self.refreshControl = UIRefreshControl()
        self.refreshControl?.addTarget(self, action: #selector(refreshControlActivated(sender:)), for: .valueChanged)
       
        /
        self.clearsSelectionOnViewWillAppear = false
        /
        /
    }
   
    override func viewWillAppear(_ animated: Bool) {
        tableView.reloadData()
        /
    }
   
    override func viewWillDisappear(_ animated: Bool) {
        Schedule.saveToFile(schedules: schedules)
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        /
    }
    /
   
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return schedules.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        /
        let schedule = schedules[indexPath.row]
        cell.textLabel?.text = schedule.name
   
        return cell
    }
   
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        previousIndex = indexPath.row
    }
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        /
        /
        guard let formViewController = segue.destination as? FormViewController else { return }
       
        if let indexPath = tableView.indexPathForSelectedRow,
            segue.identifier == "editSchedule" {
            formViewController.schedule = self.schedules[indexPath.row]
        }
    }
   
    @IBAction func unwindToTable(segue: UIStoryboardSegue) {
        guard let source = segue.source as? FormViewController, let schedule = source.schedule else { return }
       
        if let indexPath = tableView.indexPathForSelectedRow {
            schedules.remove(at: indexPath.row)
            schedules.insert(schedule, at: indexPath.row)
            schedules.remove(at: indexPath.row)
            tableView.deselectRow(at: indexPath, animated: true)
            tableView.reloadData()
        } else {
            schedules.append(schedule)
        }
       
        Schedule.saveToFile(schedules: schedules)
       
    }
}

Off the top of my head, I think you will need to decode the strings "as? NSString" not "as? String", and then bridge the NSString to a String.


You can check by decoding the objects without the cast, then checking the actual class of the decoded objects.


It's one of those places where the Obj-C origins of the API can't be fully hidden.

Thanks for the reply!


Removing the casts shows that it returns an Any? type. When I decode the strings "as? NSString", I get the same result. For reference, I modified these examples provided by Apple for an emoji app. I have gone through both Apple's code an mine several times to find something I may have done wrong, but I haven't found anything, yet.


Emoji class:

import Foundation
class Emoji: NSObject, NSCoding {
    var symbol: String
    var name: String
    var detailDescription: String
    var usage: String
  
    struct PropertyKey {
        static let symbol = "symbol"
        static let name = "name"
        static let detailDescription = "detailDescription"
        static let usage = "usage"
    }
  
    static let DocumentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    static let ArchiveURL = DocumentsDirectory.appendingPathComponent("emojis")
  
    init(symbol: String, name: String, detailDescription: String, usage: String) {
        self.symbol = symbol
        self.name = name
        self.detailDescription = detailDescription
        self.usage = usage
    }
  
    static func loadFromFile() -> [Emoji]?  {
        return NSKeyedUnarchiver.unarchiveObject(withFile: Emoji.ArchiveURL.path) as? [Emoji]
    }
  
    static func loadSampleEmojis() -> [Emoji] {
        return [Emoji(symbol: "  ", name: "Grinning Face", detailDescription: "A typical smiley face.", usage: "happiness"),
                Emoji(symbol: "  ", name: "Confused Face", detailDescription: "A confused, puzzled face.", usage: "unsure what to think; displeasure"),
                Emoji(symbol: "  ", name: "Heart Eyes", detailDescription: "A smiley face with hearts for eyes.", usage: "love of something; attractive"),
                Emoji(symbol: "  ", name: "Police Officer", detailDescription: "A police officer wearing a blue cap with a gold badge. He is smiling, and eager to help.", usage: "person of authority"),
                Emoji(symbol: "  ", name: "Turtle", detailDescription: "A cute turtle.", usage: "Something slow"),
                Emoji(symbol: "  ", name: "Elephant", detailDescription: "A gray elephant.", usage: "good memory"),
                Emoji(symbol: "  ", name: "Spaghetti", detailDescription: "A plate of spaghetti.", usage: "spaghetti"),
                Emoji(symbol: "  ", name: "Die", detailDescription: "A single die.", usage: "taking a risk, chance; game"),
                Emoji(symbol: "⛺️", name: "Tent", detailDescription: "A small tent.", usage: "camping"),
                Emoji(symbol: "  ", name: "Stack of Books", detailDescription: "Three colored books stacked on each other.", usage: "homework, studying"),
                Emoji(symbol: "  ", name: "Broken Heart", detailDescription: "A red, broken heart.", usage: "extreme sadness"),
                Emoji(symbol: "  ", name: "Snore", detailDescription: "Three blue \'z\'s.", usage: "tired, sleepiness"),
                Emoji(symbol: "  ", name: "Checkered Flag", detailDescription: "A black and white checkered flag.", usage: "completion")]
    }
  
    static func saveToFile(emojis: [Emoji]) {
        NSKeyedArchiver.archiveRootObject(emojis, toFile: Emoji.ArchiveURL.path)
    }
  
    required convenience init?(coder aDecoder: NSCoder) {
      
        guard let symbol = aDecoder.decodeObject(forKey: PropertyKey.symbol) as? String,
            let name = aDecoder.decodeObject(forKey: PropertyKey.name) as? String,
            let detailDescription = aDecoder.decodeObject(forKey: PropertyKey.detailDescription) as? String,
            let usage = aDecoder.decodeObject(forKey: PropertyKey.usage) as? String
        else {
                return nil
        }
      
        self.init(symbol: symbol, name: name, detailDescription: detailDescription, usage: usage)
    }
  
    func encode(with aCoder: NSCoder) {
        aCoder.encode(symbol, forKey: PropertyKey.symbol)
        aCoder.encode(name, forKey: PropertyKey.name)
        aCoder.encode(detailDescription, forKey: PropertyKey.detailDescription)
        aCoder.encode(usage, forKey: PropertyKey.usage)
    }
}


EmojiTableViewController:

import UIKit
class EmojiTableViewController: UITableViewController {
   
    var emojis = [Emoji]()
    / 
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
   
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return emojis.count
    }
   
   
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "EmojiCell", for: indexPath) as! EmojiTableViewCell
       
        let emoji = emojis[indexPath.row]
       
        cell.update(with: emoji)
        cell.showsReorderControl = true
               
        return cell
    }
   
    @IBAction func editButtonTapped(_ sender: UIBarButtonItem) {
        let tableViewEditingMode = tableView.isEditing
       
        tableView.setEditing(!tableViewEditingMode, animated: true)
    }
   
   
    override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle {
        return .delete
    }
    /
    override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            emojis.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .automatic)
        }
        Emoji.saveToFile(emojis: emojis)
    }
   
    override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
        let movedEmoji = emojis.remove(at: fromIndexPath.row)
        emojis.insert(movedEmoji, at: to.row)
        tableView.reloadData()
        Emoji.saveToFile(emojis: emojis)
    }
   
    func refreshControlActivated(sender: UIRefreshControl) {
        tableView.reloadData()
        sender.endRefreshing()
    }
   
    override func viewDidLoad() {
        super.viewDidLoad()
       
        if let savedEmojis = Emoji.loadFromFile() {
            emojis = savedEmojis
        } else {
            emojis = Emoji.loadSampleEmojis()
        }
       
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 44.0
       
        self.refreshControl = UIRefreshControl()
        self.refreshControl?.addTarget(self, action: #selector(refreshControlActivated(sender:)), for: .valueChanged)
    }
   
    /
    @IBAction func unwindToEmojiTableView(segue: UIStoryboardSegue) {
        guard segue.identifier == "saveUnwind" else { return }
        let sourceViewController = segue.source as! AddEditEmojiTableViewController
       
        if let emoji = sourceViewController.emoji {
            if let selectedIndexPath = tableView.indexPathForSelectedRow {
                emojis[selectedIndexPath.row] = emoji
                tableView.reloadRows(at: [selectedIndexPath], with: .none)
            } else {
                let newIndexPath = IndexPath(row: emojis.count, section: 0)
                emojis.append(emoji)
                tableView.insertRows(at: [newIndexPath], with: .automatic)
            }
        }
       
        Emoji.saveToFile(emojis: emojis)
    }
   
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
       
        if segue.identifier == "EditEmoji" {
            let indexPath = tableView.indexPathForSelectedRow!
            let emoji = emojis[indexPath.row]
            let addEditEmojiTableViewController = segue.destination as! AddEditEmojiTableViewController
            addEditEmojiTableViewController.emoji = emoji
        }
    }


}
Accepted Answer

OK, so I played around with the code in a playground, and found the problem.


When you encode a Bool (using the generic "encode" method), you must decode it as "decodeBool", not "decodeObject". Currently, it's failing on that one "let", and the entire unarchive fails as a result.


Note that that returning "nil" from "init?(coder:)" is not really error handling. As far as the archiving mechanism is concerned, a nil value is a legal result, and what happens next depends on where in the object graph this happens unexpectedly. This is a long-standing weakness of the archiving mechanism, and the correct coding approach is poorly documented.


What you actually need to do is to create an unarchiver explicitly, then use:


     try unarchiver.decodeTopLevelObject (of: [Schedules].self, forKey: NSKeyedArchiveRootObjectKey) // specifying the root object type, and the standard key for it


and if any init?(coder:) method detects an error, use:


     coder.failWithError(…)
     return nil


to signal the error, which will cause the unarchiver "try" to throw an exception with your error, when control eventually gets back to the top level.


Also, I strongly recommend you don't use the path-based versions of the archive/unarchive operations. Instead, use Data methods to read or write the Data object directly (specifying a URL, not a path), and then pass the Data object to the approproate archiver/unarchiver.

Thank you so much for your help! 🙂 This issue has halted my progress for the last three days, and I am glad it works now. I changed my division property to a String since it makes more sense for my application and allows for more than two values in the future. I will try implementing your suggestions on making the code better.

NSKeyedUnarchiver Returns Empty
 
 
Q