35

this is a class Place I defined:

class Place: NSObject {

    var latitude: Double
    var longitude: Double

    init(lat: Double, lng: Double, name: String){
        self.latitude = lat
        self.longitude = lng
    }

    required init(coder aDecoder: NSCoder) {
        self.latitude = aDecoder.decodeDoubleForKey("latitude")
        self.longitude = aDecoder.decodeDoubleForKey("longitude")
    }

    func encodeWithCoder(aCoder: NSCoder!) {
        aCoder.encodeObject(latitude, forKey: "latitude")
        aCoder.encodeObject(longitude, forKey: "longitude")
    }

}

This is how I tried to save an array of Places:

var placesArray = [Place]

//...

func savePlaces() {
    NSUserDefaults.standardUserDefaults().setObject(placesArray, forKey: "places")
    println("place saved")
}

It didn't work, this is what I get on the console:

Property list invalid for format: 200 (property lists cannot contain objects of type 'CFType')

I am new to iOS, could you help me ?

SECOND EDITION

I found a solution to save the data :

func savePlaces(){
    let myData = NSKeyedArchiver.archivedDataWithRootObject(placesArray)
   NSUserDefaults.standardUserDefaults().setObject(myData, forKey: "places")
    println("place saved")
}

But I get an error when loading the data with this code :

 let placesData = NSUserDefaults.standardUserDefaults().objectForKey("places") as? NSData

 if placesData != nil {
      placesArray = NSKeyedUnarchiver.unarchiveObjectWithData(placesData!) as [Place]
 }

the error is :

[NSKeyedUnarchiver decodeDoubleForKey:]: value for key (latitude) is not a double number'

I am pretty sure I archived a Double, there is an issue with the saving/loading process

Any clue ?

Marcus Rossel
  • 3,196
  • 1
  • 26
  • 41
Cherif
  • 5,223
  • 8
  • 33
  • 54

8 Answers8

37

Swift 4

We need to serialize our swift object to save it into userDefaults.

In swift 4 we can use Codable protocol, which makes our life easy on serialization and JSON parsing

Workflow(Save swift object in UserDefaults):

  1. Confirm Codable protocol to model class(class Place : Codable).
  2. Create object of class.
  3. Serialize that class using JsonEncoder class.
  4. Save serialized(Data) object to UserDefaults.

Workflow(Get swift object from UserDefaults):

  1. Get data from UserDefaults(Which will return Serialized(Data) object)
  2. Decode Data using JsonDecoder class

Swift 4 Code:

class Place: Codable {
    var latitude: Double
    var longitude: Double

    init(lat : Double, long: Double) {
        self.latitude = lat
        self.longitude = long
    }

    public static func savePlaces(){
        var placeArray = [Place]()
        let place1 = Place(lat: 10.0, long: 12.0)
        let place2 = Place(lat: 5.0, long: 6.7)
        let place3 = Place(lat: 4.3, long: 6.7)
        placeArray.append(place1)
        placeArray.append(place2)
        placeArray.append(place3)
        let placesData = try! JSONEncoder().encode(placeArray)
        UserDefaults.standard.set(placesData, forKey: "places")
    }

    public static func getPlaces() -> [Place]?{
        let placeData = UserDefaults.standard.data(forKey: "places")
        let placeArray = try! JSONDecoder().decode([Place].self, from: placeData!)
        return placeArray
    }
}
34

From the Property List Programming Guide:

If a property-list object is a container (that is, an array or dictionary), all objects contained within it must also be property-list objects. If an array or dictionary contains objects that are not property-list objects, then you cannot save and restore the hierarchy of data using the various property-list methods and functions.

You'll need to convert the object to and from an NSData instance using NSKeyedArchiver and NSKeyedUnarchiver.

For example:

func savePlaces(){
    let placesArray = [Place(lat: 123, lng: 123, name: "hi")]
    let placesData = NSKeyedArchiver.archivedDataWithRootObject(placesArray)
    NSUserDefaults.standardUserDefaults().setObject(placesData, forKey: "places")
}

func loadPlaces(){
    let placesData = NSUserDefaults.standardUserDefaults().objectForKey("places") as? NSData

    if let placesData = placesData {
        let placesArray = NSKeyedUnarchiver.unarchiveObjectWithData(placesData) as? [Place]

        if let placesArray = placesArray {
            // do something…
        }

    }
}
Aaron Brager
  • 65,323
  • 19
  • 161
  • 287
19

Swift 3 & 4

The following is the complete example code in Swift 3 & 4.

import Foundation

class Place: NSObject, NSCoding {

    var latitude: Double
    var longitude: Double
    var name: String

    init(latitude: Double, longitude: Double, name: String) {
        self.latitude = latitude
        self.longitude = longitude
        self.name = name
    }

    required init?(coder aDecoder: NSCoder) {
        self.latitude = aDecoder.decodeDouble(forKey: "latitude")
        self.longitude = aDecoder.decodeDouble(forKey: "longitude")
        self.name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(latitude, forKey: "latitude")
        aCoder.encode(longitude, forKey: "longitude")
        aCoder.encode(name, forKey: "name")
    }
}

func savePlaces() {
    var placesArray: [Place] = []
    placesArray.append(Place(latitude: 12, longitude: 21, name: "place 1"))
    placesArray.append(Place(latitude: 23, longitude: 32, name: "place 2"))
    placesArray.append(Place(latitude: 34, longitude: 43, name: "place 3"))

    let placesData = NSKeyedArchiver.archivedData(withRootObject: placesArray)
    UserDefaults.standard.set(placesData, forKey: "places")
}

func loadPlaces() {
    guard let placesData = UserDefaults.standard.object(forKey: "places") as? NSData else {
        print("'places' not found in UserDefaults")
        return
    }

    guard let placesArray = NSKeyedUnarchiver.unarchiveObject(with: placesData as Data) as? [Place] else {
        print("Could not unarchive from placesData")
        return
    }

    for place in placesArray {
        print("")
        print("place.latitude: \(place.latitude)")
        print("place.longitude: \(place.longitude)")
        print("place.name: \(place.name)")
    }
}

 

Example Use:

savePlaces()
loadPlaces()

 

Console Output:

place.latitude: 12.0
place.longitude: 21.0
place.name: 'place 1'

place.latitude: 23.0
place.longitude: 32.0
place.name: 'place 2'

place.latitude: 34.0
place.longitude: 43.0
place.name: 'place 3'
Mobile Dan
  • 6,444
  • 1
  • 44
  • 44
  • FYI it has nothing to do with Swift 3 or 4 especially the usage of NSCoding. You might want to edit the title to Swift without the version number. – OhadM Jun 07 '18 at 10:50
  • Please add override init() { super.init() } in your Coder class in order to avoid 'cannot invoke initializer for type with no arguments' error. – Anand Gautam Sep 24 '18 at 18:34
12

In Swift 4.0+ we can use the type alias Codable which consist of 2 protocols: Decodable & Encodable.

For convenience, I've created a generic decode and encode methods that are type constrained to Codable:

extension UserDefaults {
    func decode<T : Codable>(for type : T.Type, using key : String) -> T? {
        let defaults = UserDefaults.standard
        guard let data = defaults.object(forKey: key) as? Data else {return nil}
        let decodedObject = try? PropertyListDecoder().decode(type, from: data)
        return decodedObject
    }
    
    func encode<T : Codable>(for type : T, using key : String) {
        let defaults = UserDefaults.standard
        let encodedData = try? PropertyListEncoder().encode(type)
        defaults.set(encodedData, forKey: key)
    }
}

Usage - saving an object/array/dictionary:

Let's say we have a custom object:

struct MyObject: Codable {
    let counter: Int
    let message: String
}

and we have created an instance from it:

let myObjectInstance = MyObject(counter: 10, message: "hi there")

Using the generic extension above we can now save this object as follow:

UserDefaults.standard.encode(for: myObjectInstance, using: String(describing: MyObject.self))

Saving an array of the same type:

UserDefaults.standard.encode(for:[myFirstObjectInstance, mySecondObjectInstance], using: String(describing: MyObject.self))

Saving a dictionary with that type:

let dictionary = ["HashMe" : myObjectInstance]
UserDefaults.standard.encode(for: dictionary, using: String(describing: MyObject.self))

Usage - loading an object/array/dictionary:

Loading a single object:

let myDecodedObject = UserDefaults.standard.decode(for: MyObject.self, using: String(describing: MyObject.self))

Loading an array of the same type:

let myDecodedObject = UserDefaults.standard.decode(for: [MyObject].self, using: String(describing: MyObject.self))

Loading a dictionary with that type:

let myDecodedObject = UserDefaults.standard.decode(for: ["HashMe" : myObjectInstance].self, using: String(describing: MyObject.self))
OhadM
  • 4,687
  • 1
  • 47
  • 57
8

You have prepared Place for archiving, but you are now assuming that an array of Place will be archived automatically as you store it in NSUserDefaults. It won't be. You have to archive it. The error message is telling you this. The only things that can be saved in NSUserDefaults are objects with property list types: NSArray, NSDictionary, NSString, NSData, NSDate and NSNumber. A Place object is not one of those.

Instead of trying to save the array directly, archive it. Now it is an NSData — and that is one of the property list object types:

let myData = NSKeyedArchiver.archivedDataWithRootObject(placesArray)

Now you can store myData in NSUserDefaults, because it is an NSData. Of course when you pull it out again you will also have to unarchive it to turn it from an NSData back into an array of Place.

EDIT: By the way, it occurs to me, as an afterthought, that your Place class may have to explicitly adopt the NSCoding protocol to get this to work. You seem to have omitted that step.

matt
  • 515,959
  • 87
  • 875
  • 1,141
6

Following is the code I use. Hope this will help.

Consider Sport is the object. we need to confirm Codable class to the object class as below.

class Sport: Codable {
    var id: Int!
    var name: String!
}

Then we could simply use the following code to save the array of the object using UserDefaults.

func saveSports(_ list:[Sport]) {
        UserDefaults.standard.set(try? PropertyListEncoder().encode(list), forKey:"KEY")
        UserDefaults.standard.synchronize()
    }

Now we can simply pass the list of objects as a parameter and save using method saveSports()

For fetching the saved data, we could use the following code.

func getSports() -> [Sport]? {
    if let data = UserDefaults.standard.value(forKey:"KEY") as? Data {
        let decodedSports = try? PropertyListDecoder().decode([Sport].self, from: data)
        return decodedSports
    }
    return nil
}

The method getSports() returns the saved data.

Ravindra_Bhati
  • 1,071
  • 13
  • 28
5

Swift 5

Put the following property in one of your managers:

class UserSessionManager
{
    // MARK:- Properties

    public static var shared = UserSessionManager()

    var places: [Place]
    {
        get
        {
            guard let data = UserDefaults.standard.data(forKey: "places") else { return [] }
            return (try? JSONDecoder().decode([Place].self, from: data)) ?? []
        }
        set
        {
            guard let data = try? JSONEncoder().encode(newValue) else { return }
            UserDefaults.standard.set(data, forKey: "places")
        }
    }

    // MARK:- Init

    private init(){}
}

Use

var placesArray: [Place] = []
placesArray.append(Place(latitude: 12, longitude: 21, name: "place 1"))

// save it easily!
UserSessionManager.shared.places = placesArray

// load it easily
let mySavedPlaces = UserSessionManager.shared.places
Essam Fahmi
  • 1,920
  • 24
  • 31
0

"Mobile Den" Version Worked good, but some type deprecated in 12.0 version. I refresh this class. SWIFT 5

import Foundation

class Place: NSObject, NSCoding {

    var latitude: Double
    var longitude: Double
    var name: String

    init(latitude: Double, longitude: Double, name: String) {
        self.latitude = latitude
        self.longitude = longitude
        self.name = name
    }

    required init?(coder aDecoder: NSCoder) {
        self.latitude = aDecoder.decodeDouble(forKey: "latitude")
        self.longitude = aDecoder.decodeDouble(forKey: "longitude")
        self.name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(latitude, forKey: "latitude")
        aCoder.encode(longitude, forKey: "longitude")
        aCoder.encode(name, forKey: "name")
    }
}

func savePlaces() {
    
    var placesArray: [Place] = []
    placesArray.append(Place(latitude: 12, longitude: 21, name: "place 1"))
    placesArray.append(Place(latitude: 23, longitude: 32, name: "place 2"))
    placesArray.append(Place(latitude: 34, longitude: 43, name: "place 3"))

    do {
        let placesData = try NSKeyedArchiver.archivedData(withRootObject: placesArray, requiringSecureCoding: false)
        UserDefaults.standard.set(placesData, forKey: "places")
    } catch {
        print(error.localizedDescription)
    }
 
}

func loadPlaces() {
    
    guard let placesData = UserDefaults.standard.object(forKey: "places") as? NSData else {
        print("'places' not found in UserDefaults")
        return
    }
    
    do {
        guard let placesArray = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(placesData as Data) as? [Place] else { return }
        for place in placesArray {
            print("place.latitude: \(place.latitude)")
            print("place.longitude: \(place.longitude)")
            print("place.name: \(place.name)")
        }
    } catch {
        print(error.localizedDescription)
    }

}
Evgeniy
  • 96
  • 1
  • 4