Attempt to insert non-property list object when trying to save a custom object in Swift 3

IosSwiftSwift3NsuserdefaultsNscoding

Ios Problem Overview


I have a simple object which conforms to the NSCoding protocol.

import Foundation

class JobCategory: NSObject, NSCoding {
    var id: Int
    var name: String
    var URLString: String
    
    init(id: Int, name: String, URLString: String) {
        self.id = id
        self.name = name
        self.URLString = URLString
    }
    
    // MARK: - NSCoding
    required init(coder aDecoder: NSCoder) {
        id = aDecoder.decodeObject(forKey: "id") as? Int ?? aDecoder.decodeInteger(forKey: "id")
        name = aDecoder.decodeObject(forKey: "name") as! String
        URLString = aDecoder.decodeObject(forKey: "URLString") as! String
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(id, forKey: "id")
        aCoder.encode(name, forKey: "name")
        aCoder.encode(URLString, forKey: "URLString")
    }
}

I'm trying to save an instance of it in UserDefaults but it keeps failing with the following error.

> Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object for key jobCategory'

This is the code where I'm saving in UserDefaults.

enum UserDefaultsKeys: String {
    case jobCategory
}

class ViewController: UIViewController {
    
    @IBAction func didTapSaveButton(_ sender: UIButton) {
        let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
        
        let userDefaults = UserDefaults.standard
        userDefaults.set(category, forKey: UserDefaultsKeys.jobCategory.rawValue)
        userDefaults.synchronize()
    }
}

I replaced the enum value to key with a normal string but the same error still occurs. Any idea what's causing this?

Ios Solutions


Solution 1 - Ios

You need to create Data instance from your JobCategory instance using archivedData(withRootObject:) and store that Data instance in UserDefaults and later unarchive using unarchiveTopLevelObjectWithData(_:), So try like this.

For Storing data in UserDefaults

let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let encodedData = NSKeyedArchiver.archivedData(withRootObject: category, requiringSecureCoding: false)
let userDefaults = UserDefaults.standard
userDefaults.set(encodedData, forKey: UserDefaultsKeys.jobCategory.rawValue)

For retrieving data from UserDefaults

let decoded  = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as! Data
let decodedTeams = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! JobCategory
print(decodedTeams.name)

Solution 2 - Ios

Update Swift 4, Xcode 10

I have written a struct around it for easy access.

//set, get & remove User own profile in cache
struct UserProfileCache {
    static let key = "userProfileCache"
    static func save(_ value: Profile!) {
         UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key)
    }
    static func get() -> Profile! {
        var userData: Profile!
        if let data = UserDefaults.standard.value(forKey: key) as? Data {
            userData = try? PropertyListDecoder().decode(Profile.self, from: data)
            return userData!
        } else {
            return userData
        }
    }
    static func remove() {
        UserDefaults.standard.removeObject(forKey: key)
    }
}

> Profile is a Json encoded object.

struct Profile: Codable {
let id: Int!
let firstName: String
let dob: String!
}

> Usage:

//save details in user defaults...
UserProfileCache.save(profileDetails)

Hope that helps!!!

Thanks

Solution 3 - Ios

> Save dictionary Into userdefault

let data = NSKeyedArchiver.archivedData(withRootObject: DictionaryData)
UserDefaults.standard.set(data, forKey: kUserData)

> Retrieving the dictionary

let outData = UserDefaults.standard.data(forKey: kUserData)
let dict = NSKeyedUnarchiver.unarchiveObject(with: outData!) as! NSDictionary

Solution 4 - Ios

Swift save Codable object to UserDefault with @propertyWrapper

@propertyWrapper
    struct UserDefault<T: Codable> {
        let key: String
        let defaultValue: T
    
        init(_ key: String, defaultValue: T) {
            self.key = key
            self.defaultValue = defaultValue
        }
    
        var wrappedValue: T {
            get {
    
                if let data = UserDefaults.standard.object(forKey: key) as? Data,
                    let user = try? JSONDecoder().decode(T.self, from: data) {
                    return user
    
                }
    
                return  defaultValue
            }
            set {
                if let encoded = try? JSONEncoder().encode(newValue) {
                    UserDefaults.standard.set(encoded, forKey: key)
                }
            }
        }
    }




enum GlobalSettings {

    @UserDefault("user", defaultValue: User(name:"",pass:"")) static var user: User
}

Example User model confirm Codable

struct User:Codable {
    let name:String
    let pass:String
}

How to use it

//Set value 
 GlobalSettings.user = User(name: "Ahmed", pass: "Ahmed")

//GetValue
print(GlobalSettings.user)

Solution 5 - Ios

Based on Harjot Singh answer. I've used like this:

struct AppData {

    static var myObject: MyObject? {

        get {
            if UserDefaults.standard.object(forKey: "UserLocationKey") != nil {
                if let data = UserDefaults.standard.value(forKey: "UserLocationKey") as? Data {
                    let myObject = try? PropertyListDecoder().decode(MyObject.self, from: data)
                    return myObject!
                }
            }
            return nil
        }

        set {
            UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "UserLocationKey")
        }

    }
}

Solution 6 - Ios

Here's a UserDefaults extension to set and get a Codable object, and keep it human-readable in the plist (User Defaults) if you open it as a plain text file:

extension Encodable {
    var asDictionary: [String: Any]? {
        guard let data = try? JSONEncoder().encode(self) else { return nil }
        return try? JSONSerialization.jsonObject(with: data) as? [String : Any]
    }
}

extension Decodable {
    init?(dictionary: [String: Any]) {
        guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil }
        guard let object = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
        self = object
    }
}

extension UserDefaults {
    func setEncodableAsDictionary<T: Encodable>(_ encodable: T, for key: String) {
        self.set(encodable.asDictionary, forKey: key)
    }

    func getDecodableFromDictionary<T: Decodable>(for key: String) -> T? {
        guard let dictionary = self.dictionary(forKey: key) else {
            return nil
        }
        return T(dictionary: dictionary)
    }
}

If you want to also support array (of codables) to and from plist array, add the following to the extension:

extension UserDefaults {
    func setEncodablesAsArrayOfDictionaries<T: Encodable>(_ encodables: Array<T>, for key: String) {
        let arrayOfDictionaries = encodables.map({ $0.asDictionary })
        self.set(arrayOfDictionaries, forKey: key)
    }

    func getDecodablesFromArrayOfDictionaries<T: Decodable>(for key: String) -> [T]? {
        guard let arrayOfDictionaries = self.array(forKey: key) as? [[String: Any]] else {
            return nil
        }
        return arrayOfDictionaries.compactMap({ T(dictionary: $0) })
    }
}

If you don't care about plist being human-readable, it can be simply saved as Data (will look like random string if opened as plain text):

extension UserDefaults {
    func setEncodable<T: Encodable>(_ encodable: T, for key: String) throws {
        let data = try PropertyListEncoder().encode(encodable)
        self.set(data, forKey: key)
    }

    func getDecodable<T: Decodable>(for key: String) -> T? {
        guard
            self.object(forKey: key) != nil,
            let data = self.value(forKey: key) as? Data
        else {
            return nil
        }

        let obj = try? PropertyListDecoder().decode(T.self, from: data)
        return obj
    }
}

(With this second approach, you don't need the Encodable and Decodable extensions from the top)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionIsuruView Question on Stackoverflow
Solution 1 - IosNirav DView Answer on Stackoverflow
Solution 2 - IosHarjot SinghView Answer on Stackoverflow
Solution 3 - IosNiraj PaulView Answer on Stackoverflow
Solution 4 - IosdimohamdyView Answer on Stackoverflow
Solution 5 - IosTomasView Answer on Stackoverflow
Solution 6 - IosNikolay SuvandzhievView Answer on Stackoverflow