NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class
Asked Answered
R

2

12

I have one app with multiple targets (each target is for another client as separate application with different name, bundle identifier etc).

I have method:

fileprivate static func loadSessionFromKeychain() -> UserSession? {
    if let sessionData = KeychainWrapper.standard.data(forKey: UserSession.sessionDefaultsKey) {

        print("sessionData:")
        print(sessionData.debugDescription)
            if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession {
                _current = session
                return session
            } else {
                print("ERROR: Could not parse UserSession from Keychain")
            }
        return nil
    }
    return nil
}

The line if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession { throws error:

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

I've tried to catch do {} catch {} but it didn't catch and throws still the same error + xCode says

catch' block is unreachable because no errors are thrown in 'do' block`

Any ideas how to fix this?

UserSessionSwift.swift

    import UIKit
    import SwiftKeychainWrapper

    class UserSession: NSObject, NSCoding {
        // Static vars
        fileprivate static var _current: UserSession?
        static var current: UserSession? {
            get {
                // If there is already a session return it
                if _current != nil {
                    return _current
                }
                // If there is no session yet but one is persistently stored return it
                if let session = self.persistentLoadCurrentSession() {
                    _current = session
                    self.persistentLoadCookies()
                    return session
                }
                // Otherwise return nil
                return nil
            }
            set(value) {
                // Store the actual value
                _current = value

                // Perform hooks after changing the current session
                if value == nil {
                    self.afterLogout()
                } else {
                    self.afterLogin()
                }
            }
        }
        // Constants
        fileprivate static let cookiesDefaultsKey: String = "NSUserDefaultsKeyCookieStorage"
        fileprivate static let sessionDefaultsKey: String = "NSUserDefaultsKeyUserSessionStorage"
        // Instance properties
        let client: Client


        // -------------------------------------------------------------------------------
        // MARK: - Lifecycle
        // -------------------------------------------------------------------------------

        required init(client: Client) {
            // set local properties
            self.client = client

            // call super init
            super.init()

            // Store cookies after a session was initialized
            UserSession.persistentStoreCookies()
        }

        required init?(coder aDecoder: NSCoder) {
            self.client = aDecoder.decodeObject(forKey: "client") as! Client
            super.init()
        }


        // -------------------------------------------------------------------------------
        // MARK: - Public
        // -------------------------------------------------------------------------------

        func encode(with aCoder: NSCoder) {
            aCoder.encode(self.client, forKey: "client")
        }

        /**
         Performs all necessary operations after user logs in: stores current cookies and user session for the case user stops and reruns the application later
         */
        static func afterLogin() {
            // Persistently store session data
            self.persistentStoreCookies()
            self.persistentStoreCurrentSession()

            // Register user & device for PUSH notifications
            NotificationsManager.registerForNotifications()
        }

        /**
         Performs all necessary operations after user logs out: deletes stored cookies and user session so that the next time the user runs this application he gets the login prompt
         */
        static func afterLogout() {
            // Erase user session data
            self.persistentEraseCookies()
            self.persistentEraseCurrentSession()

            // Delete all offers from local database
            CoreDataHelper.deleteEntitiesInContext(CoreDataHelper.mainContext, entityName: UsedOffer.entityName)
            CoreDataHelper.saveContext()
        }

        static func requestPopup() {
            // Get popup from server
            print("INFO: Checking for popups on the server...")
            ClientPopupRequest.send({ (popup) -> Void in
                if let popup = popup {
                    // If there is one, show it
                    popup.showAlertAndPerform(in: RootVC.sharedInstance) {
                        // After the popup performs its action, ask for another one
                        self.requestPopup()
                    }
                } else {
                    // If none, exit
                    print("INFO: No new popups found.")
                }
            }) { (error) -> Void in
            }
        }


        // -------------------------------------------------------------------------------
        // MARK: - Private
        // -------------------------------------------------------------------------------

        /**
         Saves current user session to persistent store (currently NSUserDefaults)
         */
        static func persistentStoreCurrentSession() {
            if let session = _current {
                // Archive session
                let sessionData = NSMutableData()
                let archiver = NSKeyedArchiver(forWritingWith: sessionData)
                archiver.encode(session)
                archiver.finishEncoding()
                // Session encoded

                KeychainWrapper.standard.set(session, forKey: UserSession.sessionDefaultsKey)

    //            UserDefaults.standard.set(sessionData, forKey: UserSession.sessionDefaultsKey)
    //            UserDefaults.standard.synchronize()
            } else {
                print("WARNING: No session to store")
            }
        }

        /**
         Tries to load an user session from persistent store (currently NSUserDefaults) and store it as current session in UserSession class. Returns the loaded instance of user session if it succeeds, otherwise returns nil
         */
        fileprivate static func persistentLoadCurrentSession() -> UserSession? {
            if let keychainData = loadSessionFromKeychain() {
                persistentEraseUserDataSession()

                return keychainData
            } else if let userData = loadSessionFromStore() {
                return userData
            }
            return nil
        }

        fileprivate static func loadSessionFromKeychain() -> UserSession? {
            if let sessionData = KeychainWrapper.standard.data(forKey: UserSession.sessionDefaultsKey) {

                print("sessionData:")
                print(sessionData.debugDescription)
                if let session = NSKeyedUnarchiver.unarchiveObject(with: sessionData) as? UserSession {
                    _current = session
                    return session
                } else {
                    print("ERROR: Could not parse UserSession from Keychain")
                }
                return nil
            }
            return nil
        }

        fileprivate static func loadSessionFromStore() -> UserSession? {
            if let sessionData = UserDefaults.standard.object(forKey: UserSession.sessionDefaultsKey) as? Data {
                let unarchiver = NSKeyedUnarchiver(forReadingWith: sessionData)
                if let session = unarchiver.decodeObject() as? UserSession {
                    unarchiver.finishDecoding()
                    // Session decoded

                    _current = session
                    return session
                } else {
                    print("ERROR: Could not parse UserSession from Store")
                }
                return nil
            }
            return nil
        }

        fileprivate static func persistentEraseCurrentSession() {
            // Remove the current session object
            _current = nil

            // Remove the persisted session object
            UserDefaults.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
            KeychainWrapper.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
        }

        fileprivate static func persistentEraseUserDataSession() {
            // Remove the persisted session object
            UserDefaults.standard.removeObject(forKey: UserSession.sessionDefaultsKey)
        }

        fileprivate static func persistentStoreCookies() {
            if let cookies = HTTPCookieStorage.shared.cookies {
                let cookieData = NSKeyedArchiver.archivedData(withRootObject: cookies)

                UserDefaults.standard.set(cookieData, forKey: UserSession.sessionDefaultsKey)
                KeychainWrapper.standard.set(cookieData, forKey: UserSession.cookiesDefaultsKey)
            } else {
                print("WARNING: No cookies to store")
            }
        }

        fileprivate static func persistentLoadCookies() {

            var cookieData: Data?

            if let keychainData = KeychainWrapper.standard.data(forKey: UserSession.cookiesDefaultsKey) {
                cookieData = keychainData
            } else if let userData = UserDefaults.standard.object(forKey: UserSession.cookiesDefaultsKey) as? Data {
                cookieData = userData
            }

            if (cookieData != nil) {
                if let cookies = NSKeyedUnarchiver.unarchiveObject(with: cookieData!) as? [HTTPCookie] {
                    cookies.forEach { HTTPCookieStorage.shared.setCookie($0) }
                } else {
                    print("ERROR: Could not parse [NSHTTPCookie] from unarchived data")
                }
            } else {
                print("WARNING: No cookies to load")
            }
        }

        fileprivate static func persistentEraseCookies() {
            UserDefaults.standard.removeObject(forKey: UserSession.cookiesDefaultsKey)
            KeychainWrapper.standard.removeObject(forKey: UserSession.cookiesDefaultsKey)
        }
    }

// EDIT: added UserSession.swift class

Rosol answered 7/2, 2018 at 14:31 Comment(5)
Can you show how the UserSession object is defined?Cadency
The error message isn't complaining about your source code, it's having trouble with your UserSession class at run time. Is that class included in the target you're building?Delinquent
Yes, it's in the target. I've added UserSession.swift above.Rosol
Did you encode this object and then change the name of the class? Or the name of the project? Also, you say "one app with multiple targets" - but isn't that exactly the problem. You say "it's in the target" but what target? It probably isn't in the right target.Lustrous
I'm using this code shared in 4 targets = 4 apps with different bundle identifier. I didn't change name of the project.Rosol
L
37

What you're getting here is an exception; exceptions cannot be caught or handled in Swift, and are different from errors, which is why you can't wrap the call in a do {} catch {}.

The issue here is that your archive contains the name of a class which is then not available at runtime, which can happen for several reasons:

  1. You encoded the archive in an app that contains the class, and are attempting to decode in a different app which does not contain the class. This can happen if you forget to link the class implementation with the target you're working with, but this is much less likely in Swift because you can't import the header and forget to link the implementation
  2. The class name has changed. This can happen for a few reasons itself, but in Swift, the most likely reason is due to your app/module name changing. Classes in Swift have runtime names which include the full path to the class. If you've got an app named "MyApp", a class called "Foo" has a qualified name of "MyApp.Foo". Similarly, a class "Bar" nested in "Foo" would have a qualified name of "MyApp.Foo.Bar". Importantly, if you change the name of your app (which is the name of your main module), the name of the class changes!

What's likely happening here is that you've either renamed the target since the archive was written (which would change the class name), or you wrote the archive with the class in one target, but are decoding in another. Even though you include the same class in both, they have different names ("MyTarget1.UserSession" vs. "MyTarget2.UserSession").

You can remedy this with a few steps:

  1. Give the class a stable name which won't change with @objc, e.g. @objc(UserSession) class UserSession { ... }. This will give the class an Objective-C name that is constant and does not depend on the module name in any way
  2. Use NSKeyedUnarchiver.setClass(_:forClassName:) to migrate the old archives to use the new, stable class

See NSKeyedArchiver and sharing a custom class between targets for the full details on how to migrate the archive forward.

Louvain answered 7/2, 2018 at 15:35 Comment(4)
Thank you very much for the explanation, I'm going to implement it, test and accept the answer! :)Rosol
Jup, for my case it was the 2nd reason - I've changed the name of the target. This answer should have more upvotes!Polanco
Great explanation, thank you! I had the problem archiving an object into the general pasteboard from App1 and unarchiving it from App2. As you said the name of the serialized object class changed and in App2 the unarchiver failed even though the object class were copied in both apps.Ki
@AustinConlon Thanks! :DLouvain
M
0

Itai's answer is fantastic and I cannot match their explanation. In my case, the main target was looking for a class that existed only in the test target. The solution was to run all tests to completion and the build the main target again. I guess some cleanup hadn't been done the last time I ran my tests.

Mollie answered 30/1, 2019 at 4:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.