NSKeyedUnarchiver crashes swift 3 on the second run

2.2k views Asked by At

I am trying to save custom object to UserDefaults and I'm using this as a source code. It crashes immediately on the get part. This is my code:

class Settings {

static let defaults:UserDefaults = UserDefaults.standard

///VIP object:
class VIP: NSObject, NSCoding {
    let email: String
    let name: String
    let relation: String

    init(email: String, name: String, relation: String) {
        self.email = email
        self.name = name
        self.relation = relation
    }
    required init(coder decoder: NSCoder) {
        self.email = decoder.decodeObject(forKey: "email") as! String 
        self.name = decoder.decodeObject(forKey: "name") as! String
        self.relation = decoder.decodeObject(forKey: "relation") as! String
    }

    func encode(with coder: NSCoder) {
        coder.encode(email, forKey: "email")
        coder.encode(name, forKey: "name")
        coder.encode(relation, forKey: "relation")
    }
}


///access VIPs array with this
static var VIPs: [Settings.VIP] {
    get {

        ////CRASH (vips is not nil, and some amount of bytes)
        if let vips = self.defaults.data(forKey: "VIPs"), let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: vips) as? [Settings.VIP] {
            myPeopleList.forEach({print($0.email)})
                return myPeopleList
            } else {
                return []
            }
        }
    set {
        let encodedData = NSKeyedArchiver.archivedData(withRootObject: newValue)
        self.defaults.set(encodedData, forKey: "VIPs")
        }
    }
}

Not sure if I am missing something, the String saving and retrieving succeeds, but not this custom object.

Slightly rewriting the get method like this:

get {

        if let vips = self.defaults.data(forKey: "VIPs"){
            print("counting: ", vips.count)

            if let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: vips) as? [Settings.VIP]{
                myPeopleList.forEach({print($0.email)})
            }
        }

helps me to understand that it crashes on the NSKeyedUnarchiver.unarchiveObject() part. But the reason is unclear.

Log of the error:

Date/Time:           2017-09-12 21:00:43.4970 +0200
Launch Time:         2017-09-12 21:00:43.1623 +0200
OS Version:          iPhone OS 10.3.3 (14G5037b)
Report Version:      104

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Triggered by Thread:  2

Application Specific Information:
abort() called

Filtered syslog:
None found

Last Exception Backtrace:
0   CoreFoundation                  0x18f0c6fe0 __exceptionPreprocess + 124
1   libobjc.A.dylib                 0x18db28538 objc_exception_throw + 56
2   Foundation                      0x18fb4ab0c -[NSCoder(Exceptions) __failWithException:] + 132
3   Foundation                      0x18fb4acc0 -[NSCoder(Exceptions) __failWithExceptionName:errorCode:format:] + 436
4   Foundation                      0x18fb16dac _decodeObjectBinary + 408
5   Foundation                      0x18fb1df10 -[NSKeyedUnarchiver _decodeArrayOfObjectsForKey:] + 1544
6   Foundation                      0x18fab384c -[NSArray(NSArray) initWithCoder:] + 216
7   Foundation                      0x18fb17430 _decodeObjectBinary + 2076
8   Foundation                      0x18fb16b68 _decodeObject + 308
9   Foundation                      0x18fb15d94 +[NSKeyedUnarchiver unarchiveObjectWithData:] + 88
10  Prep                            0x100097274 0x100014000 + 537204
11  Prep                            0x100078384 0x100014000 + 410500
12  Prep                            0x100078bd8 0x100014000 + 412632
13  Prep                            0x1000a4370 0x100014000 + 590704
14  Prep                            0x1001084ac 0x100014000 + 1000620
15  Prep                            0x10010b180 0x100014000 + 1012096
16  Prep                            0x100044d28 0x100014000 + 199976
17  TCC                             0x1912d92f8 __TCCAccessRequest_block_invoke.73 + 492
18  TCC                             0x1912dc3d4 __tccd_send_block_invoke + 340
19  libxpc.dylib                    0x18e1baf30 _xpc_connection_reply_callout + 80
20  libxpc.dylib                    0x18e1baea0 _xpc_connection_call_reply + 40
21  libdispatch.dylib               0x18df7e9a0 _dispatch_client_callout + 16
22  libdispatch.dylib               0x18df8d0d4 _dispatch_queue_override_invoke + 644
23  libdispatch.dylib               0x18df8ea50 _dispatch_root_queue_drain + 540
24  libdispatch.dylib               0x18df8e7d0 _dispatch_worker_thread3 + 124
25  libsystem_pthread.dylib         0x18e187100 _pthread_wqthread + 1096
26  libsystem_pthread.dylib         0x18e186cac start_wqthread + 4

UPDATE:

It happens only on second run from xcode, so running first time - it works, and trying to run again - crashes with the described output. So there must be something with the storing of the object?

3

There are 3 answers

0
Prashant Tukadiya On BEST ANSWER

According to https://stackoverflow.com/a/4197319/4601900 The problem is that storing custom classes as a node in the Settings plist (NSUserDefaults) is not allowed, since the data is stored in the file system rather than an object from the application. The settings app (where this data will also be visible) has no idea what a "VIP" object is.

I have faced same issue and I managed to solve it with saving data physically in file and fetching from location where it saved

I am storing 2-3 Values to dynamically as per key

I have created Generic functions which were helpful to me.

let LocalServiceCacheDownloadDir        = "LocalData"
// Put your keys here
enum WSCacheKeys:String {
    case Key1 = "Test1"
    case Key2 = "Test2"
    case Key3 = "Test3"

}


func getBaseForCacheLocal(with fileName:String) -> String? {

        let filePath = FileManager.default.getDocumentPath(forItemName: self.LocalServiceCacheDownloadDir)
        if FileManager.default.directoryExists(atPath: filePath) {
            return filePath.stringByAppendingPathComponent(fileName)
        } else {
            if  FileManager.default.createDirectory(withFolderName: self.LocalServiceCacheDownloadDir) {
                return filePath.stringByAppendingPathComponent(fileName)
            }
        }
        return nil
    }



    //------------------------------------------------------------

    func cacheDataToLocal<T>(with Object:T,to key:WSCacheKeys) -> Bool {
        let success = NSKeyedArchiver.archiveRootObject(Object, toFile: getBaseForCacheLocal(with: key.rawValue)!)
        if success {
            Logger.success(s: "Local Data Cached\(String(describing: getBaseForCacheLocal(with: key.rawValue)))" as AnyObject)
        } else {
            Logger.error(s: "Sorry Failed")
        }

        return success

    }

    //------------------------------------------------------------

     func loadCachedDataFromLocal<T>(with key:WSCacheKeys ) -> [T]? {
        return NSKeyedUnarchiver.unarchiveObject(withFile: getBaseForCacheLocal(with: key.rawValue)!) as? [T]
    }


    //------------------------------------------------------------

You can fetch data like

    let objects:[VIP]? = DataManager.sharedManager.loadCachedDataFromLocal(with: .Key1)

How I Save when response comes from webservice

  self.cacheDataToLocal(with: response.result.value!, to: .Key1)

Hope it is helpful to you

0
Narendra Kumar R On

I tried this locally on Swift Playground and it works well. Nothing gets crashed. But Xcode suggested me to add @objc before the VIP class name. Please make sure that we are setting the values into VIPs variable correctly.

class Settings {

    static let defaults:UserDefaults = UserDefaults.standard

    ///VIP object:
    @objc(_TtCC10Playground8Settings3VIP)class VIP: NSObject, NSCoding {
        let email: String
        let name: String
        let relation: String

        init(email: String, name: String, relation: String) {
            self.email = email
            self.name = name
            self.relation = relation
        }
        required init(coder decoder: NSCoder) {
            self.email = decoder.decodeObject(forKey: "email") as! String
            self.name = decoder.decodeObject(forKey: "name") as! String
            self.relation = decoder.decodeObject(forKey: "relation") as! String
        }

        func encode(with coder: NSCoder) {
            coder.encode(email, forKey: "email")
            coder.encode(name, forKey: "name")
            coder.encode(relation, forKey: "relation")
        }
    }


    ///access VIPs array with this
    static var VIPs: [Settings.VIP] {
        get {

            ////CRASH (vips is not nil, and some amount of bytes)
            if let vips = self.defaults.data(forKey: "VIPs"), let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: vips) as? [Settings.VIP] {
                myPeopleList.forEach({print($0.email)})
                return myPeopleList
            } else {
                return []
            }
        }
        set {
            let encodedData = NSKeyedArchiver.archivedData(withRootObject: newValue)
            self.defaults.set(encodedData, forKey: "VIPs")
        }
    }
}


let vip = Settings.VIP.init(email: "[email protected]", name: "naren", relation: "cool")

Settings.VIPs = [vip]
_ = Settings.VIPs
0
anatoliy_v On

XCode 9 suggest me to implicitly set the encoded name for the VIP class:

enter image description here

Try to make following changes and repeat your tests:

class Settings {

    static let defaults:UserDefaults = UserDefaults.standard

    ///VIP object:
    @objc(EncodedVIP)class VIP: NSObject, NSCoding {
        let email: String
        let name: String
        let relation: String

....