1
votes

I am trying to use NSUserDefaults to save an array in to my app's core data. I thought it would be good to use NSUserDefaults but the problem is that wherever I put the code that creates the default it throws up the SIGABRT error.

Here is the code that creates the default:

let levelArrayDefault = NSUserDefaults.standardUserDefaults()
    levelArrayDefault.setValue(levelsArray, forKey: "levelsArray")
    levelArrayDefault.synchronize()

levelsArray is an array of List objects:

    class List: NSObject, NSCoding {
    // MARK: Properties
    var name: String
    var AnswersArray = [Answer]()


    init?(name: String) {
        // Initialize stored properties.
        self.name = name

        if name.isEmpty {
            return nil
        }

    }

    required init(coder decoder: NSCoder){
        self.AnswersArray = (decoder.decodeObjectForKey("AA") as? [Answer])!
        self.name = (decoder.decodeObjectForKey("name") as? String)!
    }
    func encodeWithCoder(coder: NSCoder) {
        if let AnswersArray = AnswersArray { coder.encodeObject(AnswersArray, forKey: "AA") }
        if let name = name { coder.encodeObject(name, forKey: "name") }
    }


}
class Answer: NSObject, NSCoding {
    var EnglishAnswer: String = ""
    var ChineseAnswer: String = ""
    init(newEng: String, newChi: String){
        self.EnglishAnswer = newEng
        self.ChineseAnswer = newChi
    }
    required init(coder decoder: NSCoder){
        self.EnglishAnswer = (decoder.decodeObjectForKey("EnglishAnswer") as? String)!
        self.ChineseAnswer = (decoder.decodeObjectForKey("ChineseAnswer") as? String)!
    }
    func encodeWithCoder(coder: NSCoder) {
        if let EnglishAnswer = EnglishAnswer { coder.encodeObject(EnglishAnswer, forKey: "EnglishAnswer") }
        if let ChineseAnswer = ChineseAnswer { coder.encodeObject(ChineseAnswer, forKey: "ChineseAnswer") }
    }

}

How can I stop SIGABRT from popping up and get the array to be stored. Help would be much appreciated.

2
FYI - use setObject:forKey:, not setValue:forKey:. - rmaddy
You cannot store a List object or an array of List objects in user defaults. Only property list values are legal. - matt
Also, you can't store custom classes into NSUserDefaults. You need to use NSKeyedArchiver to convert it to NSData first. Please search Stack Overflow - there are many existing questions on this topic - Aaron Brager
still throwing up sigabrt error - needshelp
@needshelp edit your question and update your code - Leo Dabus

2 Answers

3
votes

You need to convert it to NSData using NSKeyedArchiver before storing it to NSUserDefaults, try like this:

update: Xcode 11.4 • Swift 5.2 or later

import UIKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let list = List(name: "Student")
        list.answers = [Answer(english: "english answer", chinese: "中文回答")]
        let data = (try? NSKeyedArchiver.archivedData(withRootObject: [list], requiringSecureCoding: false)) ?? Data()
        UserDefaults.standard.set(data, forKey: "listData")
        guard
            let loadedData = UserDefaults.standard.data(forKey: "listData"),
            let loadedArray = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(loadedData) as? [List]
            else { return }
        print(loadedData.count)
        print(loadedArray.first ?? "none")
        print(loadedArray.first?.name ?? "no name")
        print(loadedArray.first?.answers.first?.english ?? "no english")
        print(loadedArray.first?.answers.first?.chinese ?? "no chinese")
    }
}

class Answer: NSObject, NSCoding {
    let english: String
    let chinese: String
    init(english: String, chinese: String) {
        self.english = english
        self.chinese = chinese
    }
    required init(coder decoder: NSCoder) {
        self.english = decoder.decodeString(forKey: "english")
        self.chinese = decoder.decodeString(forKey: "chinese")
    }
    func encode(with coder: NSCoder) {
        coder.encode(english, forKey: "english")
        coder.encode(chinese, forKey: "chinese")
    }
}

class List: NSObject, NSCoding {
    let name: String
    fileprivate var data = Data()
    var answers: [Answer] {
        get {
            (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? [Answer] ?? []
        }
        set {
            data = (try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false)) ?? Data()
        }
    }
    init(name: String) {
        self.name = name
    }
    required init(coder decoder: NSCoder) {
        self.data = decoder.decodeData(forKey: "answersData")
        self.name = decoder.decodeString(forKey: "name")
    }
    func encode(with coder: NSCoder) {
        coder.encode(data, forKey: "answersData")
        coder.encode(name, forKey: "name")
    }
}

extension NSCoder {
    func decodeString(forKey key: String) -> String {
        return decodeObject(forKey: key) as? String ?? ""
    }
    func decodeData(forKey key: String) -> Data {
        return decodeObject(forKey: key) as? Data ?? Data()
    }
}
2
votes

If you want to save your custom object in NSUserDefaults, it's not enough to make your class NSCoding-compliant -- you have to actually encode the data into an NSData object. This is a common mistake -- see my answer to another question for a similar situation.

So, you've added NSCoding to your Answer and List classes. That's a good start. Before you continue, you should verify that you've got that step right by using a NSKeyedArchiver to encode an example of a List object containing a few Answer objects into an instance of NSData, and then use NSKeyedUnarchiver to decode that data object back into your List. Verify that everything that you care about completes the round trip with no problems. This would be an excellent place to use Xcode's testing facility -- you could write a unit test that does exactly what I've described.

Once you know you've got the NSCoding stuff right, you should modify your code so that it encodes your List as NSData and stores the resulting data object in NSUserDefaults using the -setObject:forKey: method.