How to save an array of objects to NSUserDefault with swift?

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 ?


Solution 1:

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
    }
}

Solution 2:

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…
        }

    }
}

Solution 3:

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'