66

I have this in Playground using Swift 3, Xcode 8.0:

import Foundation
class Person: NSObject, NSCoding {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    required convenience init(coder aDecoder: NSCoder) {
        let name = aDecoder.decodeObject(forKey: "name") as! String
        let age = aDecoder.decodeObject(forKey: "age") as! Int
        self.init(
            name: name,
            age: age
        )
    }
    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(age, forKey: "age")
    }
}

create array of Person

let newPerson = Person(name: "Joe", age: 10)
var people = [Person]()
people.append(newPerson)

encode the array

let encodedData = NSKeyedArchiver.archivedData(withRootObject: people)
print("encodedData: \(encodedData))")

save to userDefaults

let userDefaults: UserDefaults = UserDefaults.standard()
userDefaults.set(encodedData, forKey: "people")
userDefaults.synchronize()

check

print("saved object: \(userDefaults.object(forKey: "people"))")

retreive from userDefaults

if let data = userDefaults.object(forKey: "people") {
    let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: data as! Data)
    print("myPeopleList: \(myPeopleList)")
}else{
    print("There is an issue")
}

just check the archived data

if let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: encodedData){
   print("myPeopleList: \(myPeopleList)")
}else{
   print("There is an issue")
}

I'm not able to correctly save the data object to userDefaults, and in addition, the check at the bottom creates the error:

Fatal error: Unexpectedly found nil while unwrapping an Optional value

The "check" line also shows the saved object is nil. Is this an error in my object's NSCoder?

2
  • 2
    if let data = userDefaults.data(forKey: "people"), let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Person] { Commented Jun 23, 2016 at 2:34
  • I put this in a single page test app and same result, with your suggested change. It looks like it may be an issue with the object coder. The error I get is: fatal error: unexpectedly found nil while unwrapping an Optional value and its at the line "if let data = userDefaults.data(forKey: "people") { let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: data )" Commented Jun 23, 2016 at 4:58

5 Answers 5

94

Swift 4 or later

You can once again save/test your values in a Playground


UserDefaults need to be tested in a real project. Note: No need to force synchronize. If you want to test the coding/decoding in a playground you can save the data to a plist file in the document directory using the keyed archiver. You need also to fix some issues in your class:


class Person: NSObject, NSCoding {
    let name: String
    let age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    required init(coder decoder: NSCoder) {
        self.name = decoder.decodeObject(forKey: "name") as? String ?? ""
        self.age = decoder.decodeInteger(forKey: "age")
    }
    func encode(with coder: NSCoder) {
        coder.encode(name, forKey: "name")
        coder.encode(age, forKey: "age")
    }
}

Testing:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        do {
            // setting a value for a key
            let newPerson = Person(name: "Joe", age: 10)
            var people = [Person]()
            people.append(newPerson)
            let encodedData = try NSKeyedArchiver.archivedData(withRootObject: people, requiringSecureCoding: false)
            UserDefaults.standard.set(encodedData, forKey: "people")
            // retrieving a value for a key
            if let data = UserDefaults.standard.data(forKey: "people"),
                let myPeopleList = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Person] {
                myPeopleList.forEach({print($0.name, $0.age)})  // Joe 10
            }                    
        } catch {
            print(error)
        }
        
    }
}
Sign up to request clarification or add additional context in comments.

7 Comments

Thanks Leo, there were a couple of changes - in required init: self.age = aDecoder.decodeObject(forKey: "age") as? Int ?? 0 to set the default for age
Sorry - just the one change
@user773881 Have you tried my code as it is? I've tested it here and it was working fine
Yes I did and it works as its own project, but isn't it necessary to also use ?? for nil coalescing on "self.age = decoder.decodeInteger(forKey: "age")" to assure a default Int?
@LeoDabus, bro could you please loot at my question stackoverflow.com/questions/44667335/… ?
|
49
let age = aDecoder.decodeObject(forKey: "age") as! Int

This has been changed for Swift 3; this no longer works for value types. The correct syntax is now:

let age = aDecoder.decodeInteger(forKey: "age")

There are associated decode...() functions for various different types:

let myBool = aDecoder.decodeBoolean(forKey: "myStoredBool")
let myFloat = aDecoder.decodeFloat(forKey: "myStoredFloat")

Edit: Full list of all possible decodeXXX functions in Swift 3

Edit:

Another important note: If you have previously saved data that was encoded with an older version of Swift, those values must be decoded using decodeObject(), however once you re-encode the data using encode(...) it can no longer be decoded with decodeObject() if it's a value type. Therefore Markus Wyss's answer will allow you to handle the case where the data was encoded using either Swift version:

self.age = aDecoder.decodeObject(forKey: "age") as? Int ?? aDecoder.decodeInteger(forKey: "age")

7 Comments

Ridiculous there was no Xcode warning that old code would break in Swift 3. Thanks for posting this!
To clarify String is a value type in Swift, but there is no decodeString function ... so the old way still applies for a String. Not sure why, though.
If it's a bool, then you'd need to use the decodeBoolean() function in combination with the ?? operator, just like the Int example. self.age = aDecoder.decodeObject(forKey: "myBoolean") as? Bool ?? aDecoder.decodeBoolean(forKey: "myBoolean") Not sure why String isn't included; anything that doesn't have a dedicated function seems to still work with decodeObject()
How to manage decode for swift3 if a key does not exist ? with Swift 2 I had: if let ageTmp = decoder.decodeObject(forKey: "age") as? Int { self.age = ageTemp }else{ self.age= 0 }
You should use if(aDecoder.containsValue(forKey: "age")){...} to see if the value exists before you try to decode it.
|
23

In Swift 4:

You can use Codable to save and retrieve custom object from the Userdefaults. If you're doing it frequently then you can add as extension and use it like below.

extension UserDefaults {

   func save<T:Encodable>(customObject object: T, inKey key: String) {
       let encoder = JSONEncoder()
       if let encoded = try? encoder.encode(object) {
           self.set(encoded, forKey: key)
       }
   }

   func retrieve<T:Decodable>(object type:T.Type, fromKey key: String) -> T? {
       if let data = self.data(forKey: key) {
           let decoder = JSONDecoder()
           if let object = try? decoder.decode(type, from: data) {
               return object
           }else {
               print("Couldnt decode object")
               return nil
           }
       }else {
           print("Couldnt find key")
           return nil
       }
   }

}

Your Class must follow Codable. Its just a typealias for both Encodable & Decodable Protocol.

class UpdateProfile: Codable {
  //Your stuffs
}

Usage:

let updateProfile = UpdateProfile()

//To save the object
UserDefaults.standard.save(customObject: updateProfile, inKey: "YourKey")

//To retrieve the saved object
let obj = UserDefaults.standard.retrieve(object: UpdateProfile.self, fromKey: "YourKey")

For more Encoding and Decoding Custom types, Please go through the Apple's documentation.

Update: Swift 5 New Solution

Testable UserDefaults with Property Wrapper + Combine

@propertyWrapper struct UserDefaultsStore<T: Codable> {
    let key: String
    let storage: UserDefaultsStorage
    private let subject: PassthroughSubject<[T], Never> = .init()
    
    var wrappedValue: [T] {
        get {
            guard let data = storage.object(forKey: key) as? Data,
               let models = try? JSONDecoder().decode([T].self, from: data) else {
                return []
            }
            return models
           
        }
        set {
            if let encoded = try? JSONEncoder().encode(newValue) {
                storage.set(encoded, forKey: key)
                subject.send(wrappedValue)
            }
            
        }
    }
    
    var projectedValue: AnyPublisher<[T], Never> {
        return subject.eraseToAnyPublisher()
    }
}

protocol UserDefaultsStorage {
    func object(forKey key: String) -> Any?
    func set(_ value: Any?, forKey key: String)
}

extension UserDefaults: UserDefaultsStorage {}

struct DataToSave: Codable {}



//Usage
// Main Target
enum StorageSettings {
    @UserDefaultsStore<DataToSave>(key:"stored_data", storage: DIInjecter.userDefaults)
    static var storeSomething: [DataToSave]
    
}
struct DependenciesInjecter {
    lazy var userDefaults: UserDefaultsStorage = UserDefaults.standard
}
var DIInjecter = DependenciesInjecter()

// Test Target
class UserDefaultsStorageMock: UserDefaultsStorage {
    var values: [String: Any]
    init(values: [String : Any] = [:]) {
        self.values = values
    }

    func object(forKey key: String) -> Any? {
        return values[key]
    }

    func set(_ value: Any?, forKey key: String) {
        values[key] = value
    }
}

extension DependenciesInjecter {
    static var mock: DependenciesInjecter {
        Self(userDefaults: UserDefaultsStorageMock())
    }
}
// Put it in setup func
DIInjecter = .mock

5 Comments

great one brother.!
@ayanchakraborty Thank you..!
This is by far the newest, cleanest approach in swift. Amazing work brother.
Hello sir my protocol is Decodable. how can i save this
@Maulikshah being Decodable doesn't mean you can also encode an object. You need to use Codable which means Encodable & Decodable
12

Try this:

self.age = aDecoder.decodeObject(forKey: "age") as? Int ?? aDecoder.decodeInteger(forKey: "age")

Comments

4

In Swift 5, I would use a property wrapper to simply the code:

/// A type that adds an interface to use the user’s defaults with codable types
///
/// Example:
/// ```
/// @UserDefaultCodable(key: "nameKey", defaultValue: "Root") var name: String
/// ```
/// Adding the attribute @UserDefaultCodable the property works reading and writing from user's defaults
/// with any codable type
///
@propertyWrapper public struct UserDefaultCodable<T: Codable> {
    private let key: String
    private let defaultValue: T

    /// Initialize the key and the default value.
    public init(key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    public var wrappedValue: T {
        get {
            // Read value from UserDefaults
            guard let data = UserDefaults.standard.object(forKey: key) as? Data else {
                // Return defaultValue when no data in UserDefaults
                return defaultValue
            }

            // Convert data to the desire data type
            let value = try? JSONDecoder().decode(T.self, from: data)
            return value ?? defaultValue
        }
        set {
            // Convert newValue to data
            let data = try? JSONEncoder().encode(newValue)

            // Set value to UserDefaults
            UserDefaults.standard.set(data, forKey: key)
        }
    }
}

2 Comments

UserDefaults has a method exactly for this purpose called data(forKey:). Btw setting a default value when decoding fails it is definitely not what you want.
When encoding fails it will actually remove any existing value setting it to nil. You should handle the errors

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.