-(void)transformObjects:(NSMutableArray*)array key:(NSString*)key
{
NSMutableArray* archiveArray = [[NSMutableArray alloc]initWithCapacity:array.count];
for (Furniture *furniture in array) {
// The error occurs on the line below
NSData *furnitureEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:furniture];
[archiveArray addObject:furnitureEncodedObject];
}
NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
[userData setObject:archiveArray forKey:key];
}
Error log:
2014-03-04 10:55:27.881 AppName[10641:60b] -[Furniture encodeWithCoder:]: unrecognized selector sent to instance 0x15d43350
I have no idea why do I get "unrecognized selector sent to instance" when trying to archive an object.
You need to implement NSCoding protocol inside your Furniture object:
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:self.yourpoperty forKey:#"PROPERTY_KEY"];
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if(self = [super init]){
self.yourpoperty = [aDecoder decodeObjectForKey:#"PROPERTY_KEY"];
}
return self;
}
Basically you specify what should be written (encoded) and read from a file (decoded). Usually for each property you want to store in a file, you make same as I did here in an example.
You'll need to implement NSCoding - here is an example with an object called SNStock that has two string properties, ticker and name:
import Foundation
class SNStock: NSObject, NSCoding
{
let ticker: NSString
let name: NSString
init(ticker: NSString, name: NSString)
{
self.ticker = ticker
self.name = name
}
//MARK: NSCoding
required init(coder aDecoder: NSCoder) {
self.ticker = aDecoder.decodeObjectForKey("ticker") as! NSString
self.name = aDecoder.decodeObjectForKey("name") as! NSString
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(ticker, forKey: "ticker")
aCoder.encodeObject(name, forKey: "name")
}
//MARK: NSObjectProtocol
override func isEqual(object: AnyObject?) -> Bool {
if let object = object as? SNStock {
return self.ticker == object.ticker &&
self.name == object.name
} else {
return false
}
}
override var hash: Int {
return ticker.hashValue
}
}
For Swift 4.1 (tested code)
import UIKit
import SwiftyJSON
class UserObject: NSObject, NSCoding {
var username: String? = ""
var userID: String? = ""
var user_email: String? = ""
var name: String? = ""
var age: String? = ""
var gender: String? = ""
override init() {
super.init()
}
init(dictionary: JSON) {
//User data initialize...
username = dictionary["username"].stringValue
userID = dictionary["iUserID"].stringValue
user_email = dictionary["email"].stringValue
name = dictionary["name"].stringValue
age = dictionary["age"].stringValue
gender = dictionary["gender"].stringValue
}
required public init(coder aDecoder: NSCoder) {
username = aDecoder.decodeObject(forKey: "username") as? String
userID = aDecoder.decodeObject(forKey: "iUserID") as? String
user_email = aDecoder.decodeObject(forKey: "email") as? String
name = aDecoder.decodeObject(forKey: "name") as? String
age = aDecoder.decodeObject(forKey: "age") as? String
gender = aDecoder.decodeObject(forKey: "gender") as? String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(username, forKey: "username")
aCoder.encode(userID, forKey: "iUserID")
aCoder.encode(user_email, forKey: "email")
aCoder.encode(name, forKey: "name")
aCoder.encode(age, forKey: "age")
aCoder.encode(gender, forKey: "gender")
}
}
Note : NSCoding protocol is important
You have a custom class Furniture which you are trying to archive with NSKeyedArchiver. In order for this to work, the Furniture class needs to conform to the < NSCoding > protocol. Which means implementing the encodeWithCoder: and initWithCoder: methods.
Currently you don't implement these methods. You need to add them.
I think your Furniture class does not implement the NSCoding protocol.
Related
When try to encode my custom object in iOS swift get this error from Xcode 8.3
unrecognized selector sent to instance 0x60800166fe80
*** -[NSKeyedArchiver dealloc]: warning: NSKeyedArchiver deallocated without having had -finishEncoding called on it.
And my code like this:
import UIKit
import Foundation
class Place: NSObject {
func setCustomObject(CustomObject obj:Any,Key key:String) {
let encodedObject : Data = NSKeyedArchiver.archivedData(withRootObject: obj)
UserDefaults.standard.set(encodedObject, forKey: key)
}
}
Here's an example how to make an object to conform to NSCoding. Basically you need to provide implementation of two methods - required convenience init?(coder decoder: NSCoder) and encode(with aCoder: NSCoder)
class Book: NSObject, NSCoding {
var title: String?
var pageCount: Int?
// Memberwise initializer
init(title: String,pageCount: Int) {
self.title = title
self.pageCount = pageCount
}
// MARK: NSCoding
// Here you will try to initialize an object from archve using keys you did set in `encode` method.
required convenience init?(coder decoder: NSCoder) {
guard let title = decoder.decodeObject(forKey: "title") as? String else { return nil }
self.init(title: title, pageCount: decoder.decodeInteger(forKey: "pageCount"))
}
// Here you need to set properties to specific keys in archive
func encode(with aCoder: NSCoder) {
aCoder.encode(self.title, forKey: "title")
aCoder.encodeCInt(Int32(self.pageCount), forKey: "pageCount")
}
}
Also I would recommend changing your setCustomObject method to this:
func setCustomObject(obj:NSCoding, key:String) {
let encodedObject : Data = NSKeyedArchiver.archivedData(withRootObject: obj)
UserDefaults.standard.set(encodedObject, forKey: key)
}
This way compiler prevent you passing NSKeyedArchiver an object that does not conform to NSCoding protocol.
If you don't want to provide all properties in the init method you can use default values:
init(title : String? = nil, pageCount: Int? = nil){
self.title = title
self.pageCount = pageCount
}
Now you can just init your object without any properties. Like that Book()
Here is the solutions, you have to implement the two methods
Encode Method
func encode(with aCoder: NSCoder)
Decoding method
required init?(coder aDecoder: NSCoder)
Complete Example code
class User: NSObject , NSCoding
{
var userID : Int = 0
var name : String = ""
var firstName : String = ""
var lastName : String = ""
var username : String = ""
var email : String = ""
override init(){
super.init();
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.userID, forKey: "id");
aCoder.encode(self.firstName, forKey: "first_name");
aCoder.encode(self.lastName, forKey: "last_name");
aCoder.encode(self.name, forKey: "name");
aCoder.encode(self.username,forKey: "username");
aCoder.encode(self.email, forKey: "email");
}
required init?(coder aDecoder: NSCoder) {
super.init()
self.userID = aDecoder.decodeInteger(forKey: "id");
self.firstName = aDecoder.decodeObject(forKey: "first_name") as! String;
self.lastName = aDecoder.decodeObject(forKey: "last_name") as! String;
self.name = aDecoder.decodeObject(forKey: "name") as! String
self.username = aDecoder.decodeObject(forKey: "username") as! String
self.email = aDecoder.decodeObject(forKey: "email") as! String;
}
init(data : [String: AnyObject]) {
super.init()
self.userID = String.numberValue(data["user_id"]).intValue;
self.firstName = String.stringValue(data["first_name"]);
self.lastName = String.stringValue(data["last_name"]);
self.email = String.stringValue(data["email"]);
self.username = String.stringValue(data["user_name"]);
}
class func loadLoggedInUser() -> User {
if let archivedObject = UserDefaults.standard.object(forKey:"CurrentUserAcc"){
if let user = NSKeyedUnarchiver.unarchiveObject(with: (archivedObject as! NSData) as Data) as? User {
return user;
}
}
return User()
}
func saveUser(){
let archivedObject : NSData = NSKeyedArchiver.archivedData(withRootObject: self) as NSData
UserDefaults.standard.set(archivedObject, forKey: "CurrentUserAcc");
UserDefaults.standard.synchronize();
}
func deleteUser(){
UserDefaults.standard.set(nil, forKey: "CurrentUserAcc")
UserDefaults.standard.synchronize();
}
}
As a Protocol oriented programming concept, I have created my model with Struct.
I want to save Array of "Struct" into Userdefault. But I am having a problem in encode/decode of the array of this model.
Here is my model Struct
struct Room {
let name : String
let id : String
let booked : Bool
}
Here I created a extension like this
extension Room {
func decode() -> Room? {
let userClassObject = NSKeyedUnarchiver.unarchiveObject(withFile: RoomClass.path()) as? RoomClass
return userClassObject?.room
}
func encode() {
let personClassObject = RoomClass(room: self)
NSKeyedArchiver.archiveRootObject(personClassObject, toFile: RoomClass.path())
}
class RoomClass: NSObject, NSCoding {
var room : Room?
init(room: Room) {
self.room = room
super.init()
}
class func path() -> String {
let documentsPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first
let path = documentsPath?.appending(("/Room"))
return path!
}
func encode(with aCoder: NSCoder) {
aCoder.encode(room!.name, forKey: "name")
aCoder.encode(room!.id, forKey: "Id")
aCoder.encode(room!.booked, forKey: "booked")
}
required init?(coder aDecoder: NSCoder) {
let _name = aDecoder.decodeObject(forKey: "name") as? String
let _id = aDecoder.decodeObject(forKey: "Id") as? String
let _booked = aDecoder.decodeBool(forKey: "booked")
room = Room(name: _name!, id: _id!, booked: _booked)
super.init()
}
}
}
When I am trying to save arrRoomList(a Array of Room objects) like this
self.saveRooms(arrayRooms: arrRoomList)
I got this error
[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance
I have also tried to encode each object first and then try to save them in default, then also it gives an error.
Can anyone please guide me how to encode/decode the array of Struct in Userdefaults in a proper way without converting it into Dictionary?
you can setup the struct to use NSKeyedArchiver directly like this:
struct Room {
let name : String
let id : String
let booked : Bool
}
extension Room {
func encode() -> Data {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: data)
archiver.encode(name, forKey: "name")
archiver.encode(id, forKey: "id")
archiver.encode(booked, forKey: "booked")
archiver.finishEncoding()
return data as Data
}
init?(data: Data) {
let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
defer {
unarchiver.finishDecoding()
}
guard let name = unarchiver.decodeObject(forKey: "name") as? String else { return nil }
guard let id = unarchiver.decodeObject(forKey: "id") as? String else { return nil }
booked = unarchiver.decodeBool(forKey: "booked")
self.name = name
self.id = id
}
}
to use with UserDefaults, call like this:
// to encode to data and save to user defaults
let room = Room(name: "asdf", id: "123", booked: true)
UserDefaults.standard.set(room.encode(), forKey: "room")
// to retrieve from user defaults
if let data = UserDefaults.standard.object(forKey: "room") as? Data {
let room = Room(data: data)
}
Can save/retrieve an array of rooms like this:
func saveRooms(arrayRooms: [Room]) {
let roomsData = arrayRooms.map { $0.encode() }
UserDefaults.standard.set(roomsData, forKey: "rooms")
}
func getRooms() -> [Room]? {
guard let roomsData = UserDefaults.standard.object(forKey: "rooms") as? [Data] else { return nil }
return roomsData.flatMap { return Room(data: $0) }
}
// save 2 rooms to user defaults
let roomA = Room(name: "A", id: "123", booked: true)
let roomB = Room(name: "B", id: "asdf", booked: false)
saveRooms(arrayRooms: [roomA, roomB])
// get the rooms
print(getRooms())
You can try Model like
class CardModel: NSObject
{
let name : String
let id : String
let booked : Bool
override init()
{
self.name = ""
self.id = ""
self.booked = false
}
required init(coder aDecoder: NSCoder)
{
self.name = aDecoder.decodeObject(forKey: "name") as! String
self.id = aDecoder.decodeObject(forKey: "id") as! String
self.booked = aDecoder.decodeObject(forKey: "booked") as! Bool
}
func encodeWithCoder(_ aCoder: NSCoder)
{
aCoder.encode(name, forKey: "name")
aCoder.encode(id, forKey: "id")
aCoder.encode(booked, forKey: "booked")
}
}
Use by Creating CardModel model Object
let objCardModel = CardModel()
objCardModel.name = "Shrikant"
objCardModel.id = "8"
objCardModel.booked = true
Access by object
let userName = objCardModel.name
I have two classes : Contact and Bill. Contact has an array of type Bill.
When I persist using NSKeyedArchiver my contacts persist just fine, however, it does not persist the Bill array.
Each time I add a a Bill or a Contact I call the insertNewObject() method in persist.
Here are my classes :
/* Persist.swift */
import Foundation
class Persist {
static let sharedInstance = Persist()
let delegate = UIApplication.sharedApplication().delegate as! AppDelegate
var contactsFilePath : String {
let manager = NSFileManager.defaultManager()
let url = manager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first! as NSURL
return url.URLByAppendingPathComponent("\(delegate.userId):objectsArrayz").path!
}
func insertNewObject(){
NSKeyedArchiver.archiveRootObject(delegate.contacts!, toFile: contactsFilePath)
}
func retrieveContracts(){
if let array = NSKeyedUnarchiver.unarchiveObjectWithFile(contactsFilePath) as? [Contact] {
delegate.contacts = array
}
}
func deleteContact(rowNum : Int){
delegate.contacts!.removeAtIndex(rowNum)
NSKeyedArchiver.archiveRootObject(delegate.contacts!, toFile: contactsFilePath)
}
/* Contact.swift */
import Foundation
class Contact : NSObject, NSCoding{
var image : UIImage?
var firstName : String?
var lastName : String?
var email : String?
var phoneNumber : String?
var address : String?
var bills : [Bill]?
init(image: UIImage, firstName : String, lastName : String, email : String, phoneNumber : String, address : String, bills : [Bill]) {
self.image = image
self.firstName = firstName
self.lastName = lastName
self.email = email
self.phoneNumber = phoneNumber
self.address = address
self.bills = bills
}
// MARK: NSCoding
required convenience init?(coder decoder: NSCoder) {
guard let image = decoder.decodeObjectForKey("image") as? UIImage,
let firstName = decoder.decodeObjectForKey("firstName") as? String,
let lastName = decoder.decodeObjectForKey("lastName") as? String,
let email = decoder.decodeObjectForKey("email") as? String,
let phoneNumber = decoder.decodeObjectForKey("phoneNumber") as? String,
let address = decoder.decodeObjectForKey("address") as? String,
let bills = decoder.decodeObjectForKey("bills") as? [Bill]
else {
return nil
}
self.init(
image: image,
firstName: firstName,
lastName : lastName,
email : email,
phoneNumber : phoneNumber,
address : address,
bills : bills
)
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.image, forKey: "image")
coder.encodeObject(self.firstName, forKey: "firstName")
coder.encodeObject(self.lastName, forKey: "lastName")
coder.encodeObject(self.email, forKey: "email")
coder.encodeObject(self.phoneNumber, forKey: "phoneNumber")
coder.encodeObject(self.address, forKey: "address")
coder.encodeObject(self.bills, forKey: "bills")
}
}
/* Bill.swift*/
import Foundation
class Bill : NSObject, NSCoding{
var service : String?
var subtotal : Double?
var taxes : Double?
var total : Double?
init(service : String, subtotal : Double, taxes : Double, total: Double) {
self.service = service
self.subtotal = subtotal
self.taxes = taxes
}
// MARK: NSCoding
required convenience init?(coder decoder: NSCoder) {
guard let service = decoder.decodeObjectForKey("service") as? String,
let subtotal = decoder.decodeObjectForKey("subtotal") as? Double,
let taxes = decoder.decodeObjectForKey("taxes") as? Double,
let total = decoder.decodeObjectForKey("total") as? Double
else {
return nil
}
self.init(
service: service,
subtotal: subtotal,
taxes: taxes,
total: total
)
}
func encodeWithCoder(coder: NSCoder) {
coder.encodeObject(self.service, forKey: "service")
coder.encodeObject(self.subtotal, forKey: "subtotal")
coder.encodeObject(self.taxes, forKey: "taxes")
coder.encodeObject(self.total, forKey: "total")
}
}
Here is a guide from the apple dev docs (in objective-c):
Discussion You must return self from initWithCoder:. If you have an
advanced need that requires substituting a different object after
decoding, you can do so in awakeAfterUsingCoder:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Articles/codingobjects.html
- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
// Implementation continues
}
- (id)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_firstName = [coder decodeObjectForKey:ASCPersonFirstName];
_lastName = [coder decodeObjectForKey:ASCPersonLastName];
_height = [coder decodeFloatForKey:ASCPersonHeight];
}
return self;
}
A call to super.init() is required in the serializable class's init method.
Snippet from Bill.swift
required init?(coder decoder: NSCoder) {
super.init()
service = decoder.decodeObjectForKey("service") as? String
subtotal = decoder.decodeObjectForKey("subtotal") as? Double
taxes = decoder.decodeObjectForKey("taxes") as? Double
total = decoder.decodeObjectForKey("total") as? Double
services = decoder.decodeObjectForKey("services") as? [Service]
}
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.
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()
}
}
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.
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.
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()
}
}
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.