How to prevent getting <uninitialized> from NSUserDefaults retrieval of an object - ios

I'm using NSUserDefaults to retrieve an object that I've stored.
This is my save functionality:
let archivedObject = NSKeyedArchiver.archivedData(withRootObject: imageMetadata)
UserDefaults.standard.set(archivedObject, forKey: kUserDefaultsKey)
UserDefaults.standard.synchronize()
and then I'm retrieving the data with the following:
var checkDefaults : Data = UserDefaults.standard.object(forKey: kUserDefaultsKey) as! Data
var newObject = NSKeyedUnarchiver.unarchiveObject(with: checkDefaults)
var newImageMetadata : CCImageMetadata = newObject as! CCImageMetadata
When I inspect newObject it seems to be of type CCImageMetadata but when I force the cast it shows as <uninitialized>
Any ideas why this would be the case? It looks like it's not initialized but I'm not sure why that'd be the case

This isn't an answer to your question, it's just a suggestion for an alternative to the old keyed archiver/unarchiver.
If you feel like being more modern and Swift-y you can use this extension to store Codable objects in the user defaults.
extension UserDefaults {
func decodable<T>(type: T.Type, forKey key: String) throws -> T? where T: Decodable {
guard let data = data(forKey: key) else { return nil }
let decoder = JSONDecoder()
return try decoder.decode(type, from: data)
}
func set<T>(value: T?, forKey key: String) throws where T: Encodable {
if let value = value {
let encoder = JSONEncoder()
let data = try encoder.encode(value)
set(data, forKey: key)
} else {
removeObject(forKey: key)
}
}
}
I guess you could something similar for objects NSCoding-compliant objects. I'm not sure if this would work, but how about:
extension UserDefaults {
func decodable<T>(type: T.Type, forKey key: String) throws -> T? where T: NSObject, T: NSCoding {
guard let data = data(forKey: key) else { return nil }
return try NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: data)
}
func set<T>(value: T?, forKey key: String) throws where T: NSObject, T: NSCoding {
if let value = value {
let data = try NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false)
set(data, forKey: key)
} else {
removeObject(forKey: key)
}
}
}

Related

How to delete NSCoder Property list stored in phone memory iOS

How to delete an encoded property list stored in a file path.
I have stored user information as an encoded property list in a file path.
let loggedDetailsFilePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(SessionKeys.loggedDetails)
func saveLoggedDetails(data: LoginModel) {
do{
let data = try encoder.encode(data)
try data.write(to: loggedDetailsFilePath!)
}catch{
print("error encoding the item \(error)")
}
}
LoginModel is my Codable model
When the user is logging out from the app the property list need to be cleared or deleted
I hope this will work for you to store data in UserDefaults
extension UserDefaults {
func save<T: Codable>(_ object: T, forKey key: String) {
let encoder = JSONEncoder()
if let encodedObject = try? encoder.encode(object) {
UserDefaults.standard.set(encodedObject, forKey: key)
UserDefaults.standard.synchronize()
}
}
func getObject<T: Codable>(forKey key: String) -> T? {
if let object = UserDefaults.standard.object(forKey: key) as? Data {
let decoder = JSONDecoder()
if let decodedObject = try? decoder.decode(T.self, from: object) {
return decodedObject
}
}
return nil
}
}
Above extension is for store & fetch Codable model to UserDefaults
Here is function to store any Codable Model :-
func setCoableInUser<T: Codable>(_ object:T, key: String)-> Void{
UserDefaults.standard.save(object, forKey: key)
}
as per above function you can easily remove data from UserDefaults
func removeObjectFromDefault(_ key: String)
{
UserDefaults.standard.removeObject(forKey: key)
}

Save dictionary to UserDefaults

I'm trying to store a dictionary in UserDefaults and always get app crash when the code runs. Here is the sample code which crashes the app when it is executed. I tried to cast it as NSDictionary or make it NSDictionary initially - got the same result.
class CourseVC: UIViewController {
let test = [1:"me"]
override func viewDidLoad() {
super.viewDidLoad()
defaults.set(test, forKey: "dict1")
}
}
Dictionaries are Codable objects by default, you can use the following extensions to save them to UserDefaults
extension UserDefaults {
func object<T: Codable>(_ type: T.Type, with key: String, usingDecoder decoder: JSONDecoder = JSONDecoder()) -> T? {
guard let data = self.value(forKey: key) as? Data else { return nil }
return try? decoder.decode(type.self, from: data)
}
func set<T: Codable>(object: T, forKey key: String, usingEncoder encoder: JSONEncoder = JSONEncoder()) {
let data = try? encoder.encode(object)
self.set(data, forKey: key)
}
}
They can be used like this:
let test = [1:"me"]
UserDefaults.standard.set(object: test, forKey: "test")
let testFromDefaults = UserDefaults.standard.object([Int: String].self, with: "test")
This extension and many others are part of SwifterSwift, you might want to use it for your next iOS project :)
To store a NSDictionary (with non-string key) in NSUserDefaults you need to convert them to NSData first.
Try this
let test = [1:"me"]
override func viewDidLoad() {
super.viewDidLoad()
let data = NSKeyedArchiver.archivedData(withRootObject: test)
let defaults = UserDefaults.standard
defaults.set(data, forKey: "dict1")
if let data2 = defaults.object(forKey: "dict1") as? NSData {
let dict = NSKeyedUnarchiver.unarchiveObject(with: data2 as Data)
print(dict)
}
}

How to save custom objects that implements Codable

It's now easier with Swift 4 to encode / decode to and from JSON or Properties list.
But I can't find how to encode to Data using Codable, without using Objective-C methods initWithCoder and encodeWithCoder.
Considering this simple class:
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
How can I encode it to Data using CodingKeys and not initWithCoder and encodeWithCoder?
EDIT:
I also need to be able to deserialize objects previously saved in userdefaults using NSKeyedArchiver.
Well, you no longer need NSKeyedArchiver.
Try this:
let questionObj = Question(title: "WWDC, 2017", answer: 1,question:1)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(questionObj) {
UserDefaults.standard.set(encoded, forKey: "K_Question")
}
let decoder = JSONDecoder()
if let questionData = UserDefaults.standard.data(forKey: "K_Question"),
let question = try? decoder.decode(Question.self, from: questionData) {
print(question.title)
print(question.answer)
print(question.question)
}
Swift 5: a great simple extension for UserDefaults:
extension UserDefaults {
func save<T: Codable>(_ object: T, forKey key: String) {
let encoder = JSONEncoder()
if let encodedObject = try? encoder.encode(object) {
UserDefaults.standard.set(encodedObject, forKey: key)
UserDefaults.standard.synchronize()
}
}
func getObject<T: Codable>(forKey key: String) -> T? {
if let object = UserDefaults.standard.object(forKey: key) as? Data {
let decoder = JSONDecoder()
if let decodedObject = try? decoder.decode(T.self, from: object) {
return decodedObject
}
}
return nil
}
}
Usage
save
UserDefaults.standard.save(currentUser, forKey: "currentUser")
get
let user: User? = UserDefaults.standard.getObject(forKey: "currentUser")
Well it can be achieved via JSONEncoder and JSONDecoder.
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
let questionObj = Question(title: "Swift", answer: "Open Source",question:1)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(questionObj) {
if let json = String(data: encoded, encoding: .utf8) {
print(json)
}
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Question.self, from: encoded) {
print(decoded)
}
}
struct Question: Codable {
var title: String
var answer: Int
var question: Int
}
class UserDefaults_Question {
static let key = "myapp.trick.question"
static var value: UserDefaults_Question? {
get {
guard let data = UserDefaults.standard.data(forKey: key) else {
print("no model for key: \(key)")
return nil
}
guard let model = try? JSONDecoder().decode(UserDefaults_Question.self, from: data) else {
print("failed to decode model for key: \(key)")
return nil
}
print("did load model for key: \(key)")
return model
}
set {
guard let value = newValue, let data: Data = try? JSONEncoder().encode(value) else {
print("removing model for key: \(key)")
UserDefaults.standard.removeObject(forKey: key)
return
}
print("inserting model for key: \(key)")
UserDefaults.standard.set(data, forKey: key)
}
}
}
UserDefaults_Question.value = Question(title: "Next President", answer: 666, question: -1)

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

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?
You need to create Data instance from your JobCategory model using JSONEncoder and store that Data instance in UserDefaults and later decode using JSONDecoder.
struct JobCategory: Codable {
let id: Int
let name: String
}
// To store in UserDefaults
if let encoded = try? JSONEncoder().encode(category) {
UserDefaults.standard.set(encoded, forKey: UserDefaultsKeys.jobCategory.rawValue)
}
// Retrieve from UserDefaults
if let data = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as? Data,
let category = try? JSONDecoder().decode(JobCategory.self, from: data) {
print(category.name)
}
Old Answer
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)
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
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)
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
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")
}
}
}
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)

Save and Load Custom Dictionary - NSUserDefaults

I have an [Int:Bool] dictionary and I am trying to save it into my NSDictionary.. However, it crashes with error Attempt to set a non-property-list object
let dictionary = [Int:Bool]()
self.dictionary[2] = true
self.dictionary[3] = false
NSUserDefaults.standardUserDefaults().setObject(dictionary, forKey: "dictionary")
Also, for loading, first I tried this but error logged it strictly requires AnyObject?.
NSUserDefaults.standardUserDefaults().objectForKey("dictionary")
Then I tried this and it logged:
NSUserDefaults.standardUserDefaults().objectForKey("dictionary") as? [Int:Bool]
I also tried dictionaryForKey. I got..
NSUserDefaults.standardUserDefaults().dictionaryForKey("dictionary")
Cannot assign value to type [String: AnyObject] to type [Int:Bool]
So which one of these 2 is a better approach to take? (The values would be optional in my case I think)
1.
NSUserDefaults.standardUserDefaults().objectForKey("dictionary") as? [Int:Bool] ?? [Int:Bool]()
2.
NSUserDefaults.standardUserDefaults().objectForKey("dictionary") as? [Int:Bool])!
Swift 4
Among basic types, UserDefaults can save any object that conforms to Codable protocol. Dictionary is one of the types that implements this protocol. You don't even need to write any custom code:
let dictionary = ["name": "Adam"]
// Save to User Defaults
UserDefaults.standard.set(dictionary, forKey: "names")
// Read from User Defaults
let saved = UserDefaults.standard.value(forKey: "names") as? [String: String]
See more info about Codable
Swift 3
You can use UserDefaults to save a Dictionary as long as key and value types are types that can be represented in a plist format (NSNumber, Data, etc.). If that's not the case, we can always serialise other types to Data when writing and deserialise from Data when reading. It can be accomplished with pretty simple extension of UserDefaults using NSKeyArchiver:
extension UserDefaults {
/// Save dictionary on key
open func set<Key, Value>(dictionary: [Key: Value]?, forKey key: String) {
let data = NSKeyedArchiver.archivedData(withRootObject: dictionary as Any)
set(data, forKey: key)
}
// Retrieve dictionary for key
open func dictionary<Key, Value>(forKey key: String) -> [Key: Value]? {
guard let data = object(forKey: key) as? Data else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [Key: Value]
}
}
Now you can call these methods:
let ages = ["Adam": 25]
// Save
UserDefaults.standard.set(dictionary: ages, forKey: "ages")
// Read
let saved: [String: Int]? = UserDefaults.standard.dictionary(forKey: "ages")
print(saved) // Optional(["Adam": 25])
Swift 2
Save custom data
func setCustomDictionary(dict: [Int: Bool]) {
let keyedArch = NSKeyedArchiver.archivedDataWithRootObject(dict)
NSUserDefaults.standardUserDefaults().setObject(keyedArch, forKey: "dictionary")
}
Retrieve data
func getDictionary() -> [Int: Bool]? {
let data = NSUserDefaults.standardUserDefaults().objectForKey("dict")
let object = NSKeyedUnarchiver.unarchiveObjectWithData(data as! NSData)
return object as? [Int: Bool]
}
Usage
var customDictionary = [Int: Bool]()
customDictionary[2] = true
customDictionary[3] = false
// Store data in NSUserDefaults
setCustomDictionary(customDictionary)
// Get data from NSUserDefaults
let userDefaultsDictionary = getDictionary()
I had a similar issue, but with different types of data.
My suggestion is to convert to NSData and retrieve the data like so:
/// Save
NSUserDefaults.standardUserDefaults().setObject(NSKeyedArchiver.archivedDataWithRootObject(object), forKey: key)
/// Read
var data = NSUserDefaults.standardUserDefaults().objectForKey(key) as NSData
var object = NSKeyedUnarchiver.unarchiveObjectWithData(data) as [String: String]
(Although it is mentioned [String : String] I actually used for [[String: AnyObject]] and worked, so maybe it can work for you too!)
This is for Swift 3
func setCustomDictionary(dict: [Int: Bool]) {
let keyedArch = NSKeyedArchiver.archivedData(withRootObject: dict)
UserDefaults.standard.set(keyedArch, forKey: "dictionary")
}
func getDictionary() -> [Int: Bool]? {
let data = UserDefaults.standard.object(forKey: "dict")
let object = NSKeyedUnarchiver.unarchiveObject(with: (data as! NSData) as Data)
return object as? [Int: Bool]
}
if you neeed more types you can use generics like this:
func saveUserDefaults<T>(withKey key: String, dict: AnyObject, myType: T.Type) {
guard let dict = dict as? T else {
print("Type mismatch")
return
}
let archiver = NSKeyedArchiver.archivedData(withRootObject: dict)
UserDefaults.standard.set(archiver, forKey: key)
}
func getUserDefaults<T>(withKey key: String, myType: T.Type) -> T? {
let unarchivedObject = getUserDefaultData(withKey: key)
return unarchivedObject as? T
}
func getUserDefaultData(withKey key: String) -> Any? {
guard let data = UserDefaults.standard.object(forKey: key) else {
return nil
}
guard let retrievedData = data as? Data else {
return nil
}
return NSKeyedUnarchiver.unarchiveObject(with: retrievedData)
}
For example [Int:Int] type usage:
var customDictionary = [Int: Int]()
customDictionary[234] = 1
customDictionary[24] = 2
customDictionary[345] = 3
saveUserDefaults(withKey: "hello", dict: customDictionary as AnyObject, myType: [Int: Int].self)
let savedDictionary = getUserDefaults(withKey: "hello", myType: [Int: Int].self)
print(savedDictionary)

Resources