如何使用Swift将对象数组保存到NSUserDefault?

35

这是我定义的一个名为Place的类:

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

}

以下是我尝试保存Place数组的方法:

var placesArray = [Place]

//...

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

不起作用了,在控制台上我得到了这个:

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

我是iOS新手,你能帮我吗?

第二版

我找到了一个保存数据的解决方案:

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

但是我使用这段代码加载数据时遇到了错误:

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

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

错误信息为:

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

我相当确定我存档了一个Double,但在保存/加载过程中出现了问题。

有什么线索吗?

8个回答

37

Swift 4

我们需要将 swift 对象序列化以将其保存到 userDefaults 中。

在 Swift 4 中,我们可以使用 Codable 协议轻松进行序列化和 JSON 解析。

工作流程(将 swift 对象保存到 UserDefaults):

  1. 将 Codable 协议添加到模型类(class Place: Codable)。
  2. 创建类的对象。
  3. 使用 JsonEncoder 类对该类进行序列化。
  4. 将序列化后(Data)的对象保存到 UserDefaults。

工作流程(从 UserDefaults 获取 swift 对象):

  1. 从 UserDefaults 获取数据(返回序列化后(Data)的对象)。
  2. 使用 JsonDecoder 类对数据进行解码。

Swift 4 代码:

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

非常感谢!运行得非常完美。我想再添加一件事情,这个程序无法处理空数据。 - Madhurya Gandi
1
@MadhuryaGandi 让变量(longitude,latitude)变为可选项,以便处理nil。 - Kazi Abdullah Al Mamun

34

根据属性列表编程指南

如果一个属性列表对象是一个容器(比如数组或字典),则其内部包含的所有对象也必须是属性列表对象。如果一个数组或字典包含了非属性列表对象,那么你就不能使用各种属性列表方法和函数保存和还原数据的层次结构。

你需要使用NSKeyedArchiverNSKeyedUnarchiver将对象转换为NSData实例,并从中还原出来。

例如:

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

    }
}

1
谢谢Aaron,但是我无法正确加载数据。请查看我的第二次编辑。 - Cherif
4
看看你的代码。你将它编码为对象,但将其解码为双精度浮点数。如果它是双精度浮点数,就将其编码为双精度浮点数。 - Aaron Brager
@AaronBrager,你能解释一下forkey:中的位置是从哪里来的吗?这是可选的吗? - neda Derakhshesh
1
@nedaDerakhshesh 这不是可选的。这是一个你自己创造的密钥。它可以是任何东西,只要你在编码和解码时使用相同的密钥即可。 - Aaron Brager

19

Swift 3 & 4

以下是完整的 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)")
    }
}

使用示例:

savePlaces()
loadPlaces()

控制台输出:

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'

FYI,这与Swift 3或4无关,特别是NSCoding的使用。您可能需要编辑标题,将其更改为没有版本号的Swift。 - OhadM
请在您的Coder类中添加override init() { super.init() },以避免出现“无法调用没有参数的类型初始化器”错误。 - Anand Gautam

12
在Swift 4.0及以上版本,我们可以使用类型别名Codable,其中包括2个协议:Decodable和Encodable。
为了方便起见,我创建了一些通用的解码和编码方法,并将它们类型限制为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)
    }
}

用法 - 保存对象/数组/字典:

假设我们有一个自定义对象:

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

我们已经从中创建了一个实例:

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

使用上述通用扩展,我们现在可以将此对象保存如下:

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

保存一个相同类型的数组:

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

保存该类型的字典:
let dictionary = ["HashMe" : myObjectInstance]
UserDefaults.standard.encode(for: dictionary, using: String(describing: MyObject.self))

用法 - 加载对象/数组/字典:

加载单个对象:

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

加载一个相同类型的数组:

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

加载该类型的字典:

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

你提到了 using: String(describing: MyObject)),像这样使用 using: String(describing: MyObject.self)) 对吗? - Matrosov Oleksandr
1
当然,这只是一个示例,你可以放任何你想要的东西。 - OhadM

8
你已经准备好存档的场所,但是你现在假设一个场所数组在存储到NSUserDefaults时会自动被归档。但实际上不会。错误信息告诉你这一点。能够被保存在NSUserDefaults中的只有具有属性列表类型的对象:NSArray、NSDictionary、NSString、NSData、NSDate和NSNumber。而场所对象并不属于这些类型之一。
相反,试着将数组进行归档。现在它是NSData——而这是属性列表对象类型之一:
let myData = NSKeyedArchiver.archivedDataWithRootObject(placesArray)

现在你可以将 myData 存储在NSUserDefaults中,因为它是NSData类型。当然,当您再次提取它时,您还需要对其进行解档,以将其从NSData转换回Place数组。 编辑:顺便提一下,作为事后想到的事情,您的Place类可能必须明确采用NSCoding协议才能使其正常工作。您似乎省略了这一步。

6
以下是我所使用的代码。希望这能帮助到您。
假设Sport是该对象。我们需要将Codable类确认为对象类,如下所示。
class Sport: Codable {
    var id: Int!
    var name: String!
}

接下来,我们可以使用以下代码通过UserDefaults来保存对象数组。

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

现在我们可以将对象列表作为参数传递并使用方法saveSports()进行保存。
要获取已保存的数据,我们可以使用以下代码。
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
}

getSports()方法返回保存的数据。


5

Swift 5

把以下属性放入您的管理器之一:

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(){}
}

用途

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

0

"Mobile Den"版本运行良好,但在12.0版本中某些类型已被弃用。我更新了这个类。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)
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接