1

Swift 3,

I'm using NSUserDefaults in my iOS app to save and load the indexPath and which section the row that the action took place in. As I have a button in each row in my tableview. To be loaded again whenever it reloads the table, in ViewDidLoad.

In ViewDidLoad, I'm calling the fetch function which is supposed to save and load anything.

func fetchData() {

    // request from remote or local
    data = [testArray]

    // Update the items to first section has 0 elements,
    // and place all data in section 1
    items = [[], data ?? []]

    // apply ordering
    applySorting() { "\($0)" }

    // save ordering
    saveSorting() { "\($0)" }

    // refresh the table view
    myTableView.reloadData()
}

In my buttonAction, I am using the saveSorting() function.

func saveSorting(_ dataIdBlock: (Any) -> String) {

    guard let items = self.items else { return }

    for (section, rows) in items.enumerated() {
        for (row, item) in rows.enumerated() {
            let indexPath = IndexPath(row: row, section: section)
            let dataId = dataIdBlock(item)
            let ordering = DataHandling(dataId: dataId, indexPath: indexPath)
            ordering.save(defaults: indexPath.defaultsKey)
        }
    }
}

Here is my breakpoint picture showing the logs and where in the code it broke.

code screenshot

I would appreciate your help on fixing this crash. The app doesn't even load it stays in a white screen before the app fully loads. Thank You.

Here is the code

class DataHandling: NSObject, NSCoding {

var indexPath: IndexPath?
var dataId: String?

init(dataId: String, indexPath: IndexPath) {
    super.init()
    self.dataId = dataId
    self.indexPath = indexPath
}

required init(coder aDecoder: NSCoder) {

    if let dataId = aDecoder.decodeObject(forKey: "dataId") as? String {
        self.dataId = dataId
    }

    if let indexPath = aDecoder.decodeObject(forKey: "indexPath") as? IndexPath {
        self.indexPath = indexPath
    }

}

func encode(with aCoder: NSCoder) {
    aCoder.encode(dataId, forKey: "dataId")
    aCoder.encode(indexPath, forKey: "indexPath")
}

func save(defaults box: String) -> Bool {

    let defaults = UserDefaults.standard
    let savedData = NSKeyedArchiver.archivedData(withRootObject: self)
    defaults.set(savedData, forKey: box)
    return defaults.synchronize()

}

convenience init?(defaults box: String) {

    let defaults = UserDefaults.standard
    if let data = defaults.object(forKey: box) as? Data,
        let obj = NSKeyedUnarchiver.unarchiveObject(with: data) as? DataHandling,
        let dataId = obj.dataId,
        let indexPath = obj.indexPath {
        self.init(dataId: dataId, indexPath: indexPath)
    } else {
        return nil
    }

}

class func allSavedOrdering(_ maxRows: Int) -> [Int: [DataHandling]] {

    var result: [Int: [DataHandling]] = [:]
    for section in 0...1 {
        var rows: [DataHandling] = []
        for row in 0..<maxRows {
            let indexPath = IndexPath(row: row, section: section)
            if let ordering = DataHandling(defaults: indexPath.defaultsKey) {
                rows.append(ordering)
            }
            rows.sort(by: { $0.indexPath! < $1.indexPath! })
        }
        result[section] = rows
    }

    return result

 }

}

Error Code:

*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (CustomCellSwift.DataOrdering) for key (root); the class may be defined in source code or a library that is not linked'

JSON Code

func retrieveData() {

    let getDataURL = "http://ip/test.org/Get.php"
    let url: NSURL = NSURL(string: getDataURL)!

    do {

        let data: Data = try Data(contentsOf: url as URL)
        jsonArray = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! NSMutableArray

        // Looping through jsonArray
        for i in 0..<jsonArray.count {

            // Create Test Object
            let gID: String = (jsonArray[i] as AnyObject).object(forKey: "id") as! String
            let gName: String = (jsonArray[i] as AnyObject).object(forKey: "gameName") as! String

            // Add Test Objects to Test Array
            testArray.append(Test(gameTest: tName, andTestID: tID))

        }
    }
    catch {
        print("Error: (Getting Data)")
    }

    myTableView.reloadData()
}

applySorting Code

func applySorting(_ dataIdBlock: (Any) -> String) {

    // get all saved ordering
    guard let data = self.data else { return }
    let ordering = DataHandling.allSavedOrdering(data.count)

    var result: [[Any]] = [[], []]

    for (section, ordering) in ordering {
        guard section <= 1 else { continue } // make sure the section is 0 or 1
        let rows = data.filter({ obj -> Bool in
            return ordering.index(where: { $0.dataId == .some(dataIdBlock(obj)) }) != nil
        })
        result[section] = rows
    }

    self.items = result
}
WokerHead
  • 947
  • 2
  • 15
  • 46
  • 1
    Can you add crash log and `DataHandling` class? – Ryan Jan 20 '17 at 18:17
  • 2
    What is the actual error message? – l'L'l Jan 20 '17 at 18:19
  • @Ryan How do I get the crash log? It just saids (lldb) and I uploaded the code. – WokerHead Jan 20 '17 at 18:21
  • @l'L'l its a crash by a breakpoint, shows no error – WokerHead Jan 20 '17 at 18:22
  • You must set `Exception Breakpoint`. Turn that option off and see what actual crash message is. Can you add break point in `DataHandling`'s initWithCoder method and see where actual problem comes? – Ryan Jan 20 '17 at 18:24
  • @Ryan *** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (CustomCellSwift.DataOrdering) for key (root); the class may be defined in source code or a library that is not linked' – WokerHead Jan 20 '17 at 18:28
  • @Ryan Got anything from that crash log? – WokerHead Jan 20 '17 at 18:36
  • 1
    maybe you have old data in the defaults? did you start over and clean all from `UserDefaults`? (because it try to decode a complete different class `CustomCellSwift.DataOrdering` which you try to cast to `DataHandling`) – muescha Jan 20 '17 at 21:51
  • remove data in the iOS Simulator, you can do that via iOS Simulator > Reset Content and Settings. – muescha Jan 20 '17 at 21:56
  • you forget to include how you generate the defaultsKey from index path. maybe you use this extension also in an other userDefaults save? `extension IndexPath { var defaultsKey: String { return "data_ordering_\(section)_\(row)" } }` – muescha Jan 20 '17 at 21:57
  • ok - i see thats in an answer here http://stackoverflow.com/questions/41412168/guidance-on-core-data-with-swift-3 of your question. you can not store 2 different Classes with the same key – muescha Jan 20 '17 at 21:59
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/133721/discussion-on-question-by-brosimple-nskeyunarchiver-causing-crash). – Bhargav Rao Jan 21 '17 at 19:48

1 Answers1

0

your indexPath.defaultsKey implementation is wrong.

I see that you has done in this question an extension:

(note the following is a missing code in the original question and not my solution)

extension IndexPath { 
    var defaultsKey: String { 

        return "data_ordering_\(section)_\(row)" 
    } 
}

this line return "data_ordering_\(section)_\(row)" is the problem. you have a second class DataOrdering which are stored with this key. but you like to save also DataHandling. and then you need a different key.

you need unique keys to store in UserDefaults.

for example you should change it to:

DataHandling(defaults: "DataHandling_"+ indexPath.defaultsKey)

update

better add the class name in save(defaults box: String)

 defaults.set(savedData, forKey: "DataHandling_"+box)

and also in convenience init?(defaults box: String)

if let data = defaults.object(forKey: "DataHandling_"+box) as? Data,

(you should change this parts also in the other class u used (DataOrdering) to to make the saved keys unique for each different Class you save

Update

if you have renamed your class then only reset the UserDefaults is enough. you can do it with removing the app from simulator or just remove all temporal data from simulator when you go to this menu in the simulator: iOS Simulator > Reset Content and Settings. But to don't get an error again you should not use your current IndexPath.defaultsKey without the current class name.

a note:

you can save an array in one step to userdefaults. you dont need to read each object step by step from userdefaults. and if you save an array then the order is fixed (in my case this was the same order). i think you should post your code to https://codereview.stackexchange.com/ for more optimization of your code

Community
  • 1
  • 1
muescha
  • 1,544
  • 2
  • 12
  • 22
  • So I change my extension to the bottom code you posted? – WokerHead Jan 20 '17 at 22:09
  • my array is being filled with json objects thats why I am using this method. – WokerHead Jan 20 '17 at 22:15
  • the extension is then available global. and you have the same problem again. you need a unique key for `UserDefaults` - if you prepend always the class then it should be save – muescha Jan 20 '17 at 22:15
  • yes but changing JSON -> Array and then save the array in one step. and just save your `items` in one step – muescha Jan 20 '17 at 22:18
  • i think the answer in the other question is bad solution for your use case. – muescha Jan 20 '17 at 22:20
  • please post your complete use case what you try to do (i see you have minimum 2 classes you like to save in userdefaults) at http://codereview.stackexchange.com/ – muescha Jan 20 '17 at 22:28
  • thx for the downvote :( but i would kindly ask the downvoter to explain what is wrong with my answer. my answer focus on the current error. but the whole code is far away from perfekt and should be refactored. but the current error is solved with my answer. – muescha Jan 20 '17 at 22:34
  • Okay its not crashing anymore but my array is empty now, the tableview shows the headers of both sections but no rows inside the sections. – WokerHead Jan 20 '17 at 22:53
  • yes - you have populate and fill in the data again to UserDefauls. it is now like there is all empty and like the first start with no data – muescha Jan 20 '17 at 23:24
  • How do I refill them? Any suggestion? – WokerHead Jan 20 '17 at 23:31
  • !? you should know your app - i guess the part here: `func fetchData() { // request from remote or local data = [testArray]` and you should design your app that i works if the user start it the first time – muescha Jan 20 '17 at 23:47
  • if my answer fix your error, then it would be nice if you accept my answer (click on the grey checkmark) – muescha Jan 20 '17 at 23:57