64

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?

pkamb
  • 33,281
  • 23
  • 160
  • 191
user773881
  • 845
  • 1
  • 8
  • 10
  • 2
    `if let data = userDefaults.data(forKey: "people"), let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Person] {` – Leo Dabus Jun 23 '16 at 02: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 )" – user773881 Jun 23 '16 at 04:58

5 Answers5

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)
        }
        
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • 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 – user773881 Jun 23 '16 at 12:53
  • Sorry - just the one change – user773881 Jun 23 '16 at 12:59
  • @user773881 Have you tried my code as it is? I've tested it here and it was working fine – Leo Dabus Jun 23 '16 at 16:24
  • 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? – user773881 Jun 23 '16 at 19:54
  • @LeoDabus, bro could you please loot at my question https://stackoverflow.com/questions/44667335/how-to-pass-prefs-value-data-from-view-controller-to-inside-table-view-cell-with ? – May Phyu Jun 21 '17 at 06:51
  • @LeoDabus Any Luck regarding handling `NSKeyedUnarchiver.unarchiveObject(withFile:` when corrupted/ file not exists. Or can we apply checks ? Since `NSKeyedUnarchiver.unarchiveObject` not throws hence we can't add `try catch`. Any workaround? – Jack Mar 22 '18 at 08:41
  • Best answer in my opinion. Don't forget to import that NSCoding – DaWiseguy Apr 19 '18 at 17:53
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")
RemyNL
  • 576
  • 3
  • 8
ibuprofane
  • 639
  • 1
  • 6
  • 9
  • 19
    Ridiculous there was no Xcode warning that old code would break in Swift 3. Thanks for posting this! – Crashalot Sep 23 '16 at 06:25
  • What if the key isn't an Int but a Bool, for instance? How do you safely decode the value? – Crashalot Sep 23 '16 at 21:26
  • 1
    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. – Crashalot Sep 24 '16 at 04:02
  • 2
    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()` – ibuprofane Sep 26 '16 at 16:38
  • You just saved my day! Thanks! – Mette Oct 11 '16 at 21:37
  • 2
    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 } – Ludo Nov 07 '16 at 02:01
  • 2
    You should use if(aDecoder.containsValue(forKey: "age")){...} to see if the value exists before you try to decode it. – ibuprofane Nov 07 '16 at 17:42
  • @ibuprofane @RemyNL Is there a similar caveat for using `NSKeyedUnarchiver.unarchiveObject()` vs the various `decodeXXX` methods on `NSKeyedUnarchiver`? If so, can you provide an example? – Professor Tom Nov 19 '16 at 22:46
21

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.

Bishow Gurung
  • 1,962
  • 12
  • 15
12

Try this:

self.age = aDecoder.decodeObject(forKey: "age") as? Int ?? aDecoder.decodeInteger(forKey: "age")
mswyss
  • 322
  • 3
  • 12
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)
        }
    }
}
93sauu
  • 3,770
  • 3
  • 27
  • 43
  • `UserDefaults` has a method exactly for this purpose called [`data(forKey:)`](https://developer.apple.com/documentation/foundation/userdefaults/1409590-data). Btw setting a default value when decoding fails it is definitely not what you want. – Leo Dabus Jan 24 '22 at 17:22
  • When encoding fails it will actually remove any existing value setting it to `nil`. You should handle the errors – Leo Dabus Jan 24 '22 at 17:25