Swift 中的简单持久存储

Posted

技术标签:

【中文标题】Swift 中的简单持久存储【英文标题】:Simple persistent storage in Swift 【发布时间】:2014-12-01 16:57:30 【问题描述】:

我有一个对象数组,每个对象都有许多属性。以下是通过对象数组循环获取的一些示例数据:

Name = Rent
Default Value 750
This Months Estimate = 750
Sum Of This Months Actuals = 0
Risk Factor = 0.0
Monthly Average = 750.0
--------------
Name = Bills
Default Value 250
This Months Estimate = 170
Sum Of This Months Actuals = 140
Risk Factor = 0.0
Monthly Average = 190.0
--------------
Name = Food
Default Value 240
This Months Estimate = 200
Sum Of This Months Actuals = 95
Risk Factor = 0.0
Monthly Average = 190.0
--------------
Name = Lunches
Default Value 100
This Months Estimate = 150
Sum Of This Months Actuals = 155
Risk Factor = 0.899999976158142
Monthly Average = 190.0

它的数据很少,所以我想避免使用核心数据。我需要能够持久保存数组,然后再次打开它并能够循环遍历它。我希望使用像 NSUserDefaults 或 NSKeyedArchiver 这样的简单解决方案,但在 Swift 中,我无法使用这种类型的数组(我已经在线浏览文档、论坛和示例 24 小时......)

您如何建议我坚持保存上述对象数组?或者可能坚持保存这种类型的数组是不好的做法?

提前感谢您的帮助!

添加对象类:

class costCategory : NSObject 
    var name : String
    var defaultValue : Int
    var thisMonthsEstimate : Int
    var sumOfThisMonthsActuals : Int
    var riskFactor : Float
    var monthlyAverage : Float


    init (name:String, defaultValue:Int, thisMonthsEstimate:Int, sumOfThisMonthsActuals:Int, riskFactor:Float, monthlyAverage:Float) 
        self.name = name
        self.defaultValue = defaultValue
        self.thisMonthsEstimate = thisMonthsEstimate
        self.sumOfThisMonthsActuals = sumOfThisMonthsActuals
        self.riskFactor = riskFactor
        self.monthlyAverage = monthlyAverage
    


如果我尝试将数组保存到 NSUserDefaults,我会收到错误消息:

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

我尝试使用从 NSCoder 类继承,但我得到一个我无法解决的错误,如下所示:

class costCategory : NSObject, NSCoder 
    var name : String
    var defaultValue : Int
    var thisMonthsEstimate : Int
    var sumOfThisMonthsActuals : Int
    var riskFactor : Float
    var monthlyAverage : Float


    init (name:String, defaultValue:Int, thisMonthsEstimate:Int, sumOfThisMonthsActuals:Int, riskFactor:Float, monthlyAverage:Float) 
        self.name = name
        self.defaultValue = defaultValue
        self.thisMonthsEstimate = thisMonthsEstimate
        self.sumOfThisMonthsActuals = sumOfThisMonthsActuals
        self.riskFactor = riskFactor
        self.monthlyAverage = monthlyAverage
    



    // MARK: NSCoding

    required convenience init(coder decoder: NSCoder) 
        self.init() //Error here "missing argument for parameter name in call
        self.name = decoder.decodeObjectForKey("name") as String
        self.defaultValue = decoder.decodeIntegerForKey("defaultValue")
        self.thisMonthsEstimate = decoder.decodeIntegerForKey("thisMonthsEstimate")
        self.sumOfThisMonthsActuals = decoder.decodeIntegerForKey("sumOfThisMonthsActuals")
        self.riskFactor = decoder.decodeFloatForKey("riskFactor")
        self.monthlyAverage = decoder.decodeFloatForKey("monthlyAverage")

    

    func encodeWithCoder(coder: NSCoder) 
        coder.encodeObject(self.name, forKey: "name")
        coder.encodeInt(Int32(self.defaultValue), forKey: "defaultValue")
        coder.encodeInt(Int32(self.thisMonthsEstimate), forKey: "thisMonthsEstimate")
        coder.encodeInt(Int32(self.sumOfThisMonthsActuals), forKey: "sumOfThisMonthsActuals")
        coder.encodeFloat(self.riskFactor, forKey: "riskFactor")
        coder.encodeFloat(self.monthlyAverage, forKey: "monthlyAverage")

    

【问题讨论】:

这个是类还是值类型? 用 NSDictionary 代替 Array 怎么样?然后可以使用 NSUserDefaults 存储 NSDictionary。 您可能还想准确说明您尝试此解决方案的方式。 它们是一个非常简单的 Kirsteins 类的实例化。我确实尝试过先创建一个字典数组,但也有同样的问题——混合对象类型的字典数组无法持久保存... @user3535074 请看答案,不要使用 encodeInteger 方法强制转换为 Int32! 【参考方案1】:

Core Data 非常强大,我强烈建议您不要被其令人生畏的外观所动摇。当您的应用程序发展壮大时,您将非常感激您使用 Core Data 支持您的数据,因为它的扩展性非常好。

话虽如此,NSHipster that covers the basics of using the NSKeyedArchiver 上有一篇不错的文章。因此解决方案是让您的对象成为 NSObject 的子类并符合 NSCoding 协议。这允许您从磁盘归档和取消归档对象。您可以将文件保存到documents directory

然后,您的子类需要隐式解包所有可编码变量,结果将产生:

class costCategory : NSObject, NSCoding 
var name : String!
var defaultValue : Int!
var thisMonthsEstimate : Int!
var sumOfThisMonthsActuals : Int!
var riskFactor : Float!
var monthlyAverage : Float!


init (name:String, defaultValue:Int, thisMonthsEstimate:Int, sumOfThisMonthsActuals:Int, riskFactor:Float, monthlyAverage:Float) 
    self.name = name
    self.defaultValue = defaultValue
    self.thisMonthsEstimate = thisMonthsEstimate
    self.sumOfThisMonthsActuals = sumOfThisMonthsActuals
    self.riskFactor = riskFactor
    self.monthlyAverage = monthlyAverage


override init() 
    super.init()


// MARK: NSCoding

required convenience init(coder decoder: NSCoder) 
    self.init()
    self.name = decoder.decodeObjectForKey("name") as String
    self.defaultValue = decoder.decodeIntegerForKey("defaultValue")
    self.thisMonthsEstimate = decoder.decodeIntegerForKey("thisMonthsEstimate")
    self.sumOfThisMonthsActuals = decoder.decodeIntegerForKey("sumOfThisMonthsActuals")
    self.riskFactor = decoder.decodeFloatForKey("riskFactor")
    self.monthlyAverage = decoder.decodeFloatForKey("monthlyAverage")


func encodeWithCoder(coder: NSCoder) 
    coder.encodeObject(self.name, forKey: "name")
    coder.encodeInteger((self.defaultValue), forKey: "defaultValue")
    coder.encodeInteger((self.thisMonthsEstimate), forKey: "thisMonthsEstimate")
    coder.encodeInteger((self.sumOfThisMonthsActuals), forKey: "sumOfThisMonthsActuals")
    coder.encodeFloat(self.riskFactor, forKey: "riskFactor")
    coder.encodeFloat(self.monthlyAverage, forKey: "monthlyAverage")


然后您可以在文档目录中添加一种查找位置的方法:

func documentsDirectory() -> NSString 
    let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
    let documentDirectory = paths[0] as String
    return documentDirectory

所以归档看起来像这样:

var filePath = documentsDirectory().stringByAppendingPathComponent("fileName")
NSKeyedArchiver.archiveRootObject(receipts, toFile: filePath)

以后你可以从磁盘读回你的数组:

let receipts = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath)

我建议不要使用 User Defaults 来存储数组,它的意思是存储首选项,这里看看文档:

NSUserDefaults 类为 与默认系统交互。默认系统允许 应用程序自定义其行为以匹配用户的偏好。 例如,您可以允许用户确定哪些单位 测量您的应用程序显示或文档的频率 自动保存。应用程序通过分配记录这些偏好 用户默认数据库中的一组参数的值。

理想情况下,您不希望保留数组,因为即使您需要一个对象,也必须将整个数组提取到内存中。自然,Core Data 会为您解决所有不必要的堆问题:)

编辑

为了让自己轻松进入 Core Data,您可以随时查看 Magical Record。它们简化了维护 Core Data 堆栈所需的大量繁琐工作。

祝你好运

核心数据传播者

【讨论】:

感谢您的 cmets Daniel。恐怕我实际上已经找到了那篇文章,并花了几个小时浏览它并尝试了它解释的每种方法,但是无论我做什么,每一点都有错误。真的,我想避免使用核心数据仅仅是因为我正在尝试(但失败得很厉害)以跟上 Swift 的速度,并且我想测试我迄今为止所研究的内容。也许你是对的,也许我应该深入研究核心数据...... @user3535074 请详细说明您面临的错误类型? 我在主帖中添加了该类以及尝试保存到 NSUser 默认值时出现的错误。谢谢! 不要使用 NSUSerDefaults 来存储数组。它旨在存储简单的键值对。你想存储对象。为此,您需要将阵列存档到磁盘上的文件中。见 Kirsteins 邮报 我更新了我的答案:) 你真的应该看看 NSHipster 文章,因为你的对象没有实现他概述的任何方法【参考方案2】:

一种可能性是将您的对象属性:值转换为字符串:对象将它们存储到NSUserDefaults,然后获取并解码它们。

如果你想使用NSKeyedArchiver 存储你的对象,你的类需要符合NSCoding 并且是NSObject 的子类。示例:

class costCategory : NSObject, NSCoding 
    var name : String
    var defaultValue : Int
    var thisMonthsEstimate : Int
    var sumOfThisMonthsActuals : Int
    var riskFactor : Float
    var monthlyAverage : Float

    init (name:String, defaultValue:Int, thisMonthsEstimate:Int, sumOfThisMonthsActuals:Int, riskFactor:Float, monthlyAverage:Float) 
        self.name = name
        self.defaultValue = defaultValue
        self.thisMonthsEstimate = thisMonthsEstimate
        self.sumOfThisMonthsActuals = sumOfThisMonthsActuals
        self.riskFactor = riskFactor
        self.monthlyAverage = monthlyAverage
    

    // MARK: NSCoding

    required init(coder decoder: NSCoder) 
        //Error here "missing argument for parameter name in call
        self.name = decoder.decodeObjectForKey("name") as String
        self.defaultValue = decoder.decodeIntegerForKey("defaultValue")
        self.thisMonthsEstimate = decoder.decodeIntegerForKey("thisMonthsEstimate")
        self.sumOfThisMonthsActuals = decoder.decodeIntegerForKey("sumOfThisMonthsActuals")
        self.riskFactor = decoder.decodeFloatForKey("riskFactor")
        self.monthlyAverage = decoder.decodeFloatForKey("monthlyAverage")
        super.init()
    

    func encodeWithCoder(coder: NSCoder) 
        coder.encodeObject(self.name, forKey: "name")
        coder.encodeInt(Int32(self.defaultValue), forKey: "defaultValue")
        coder.encodeInt(Int32(self.thisMonthsEstimate), forKey: "thisMonthsEstimate")
        coder.encodeInt(Int32(self.sumOfThisMonthsActuals), forKey: "sumOfThisMonthsActuals")
        coder.encodeFloat(self.riskFactor, forKey: "riskFactor")
        coder.encodeFloat(self.monthlyAverage, forKey: "monthlyAverage")

    

然后就可以存档保存到NSDefaults

let defaults = NSUserDefaults.standardUserDefaults()
let arrayOfObjectsKey = "arrayOfObjectsKey"

var arrayOfObjects = [costCategory]()
var arrayOfObjectsData = NSKeyedArchiver.archivedDataWithRootObject(arrayOfObjects)

defaults.setObject(arrayOfObjectsData, forKey: arrayOfObjectsKey)

// ...

var arrayOfObjectsUnarchivedData = defaults.dataForKey(arrayOfObjectsKey)!
var arrayOfObjectsUnarchived = NSKeyedUnarchiver.unarchiveObjectWithData(arrayOfObjectsUnarchivedData) as [costCategory]

【讨论】:

这个不能用swift编译,你需要去掉NSCoding参数的自动解包声明 已修复。抱歉,没有测试在以前的 Xcode 6 beta 中运行的代码。 我也试过了。我在调用自定义初始化程序(使用属性值)时遇到了很多问题。我尝试但无法删除错误的完整代码已添加到主帖中(对不起,我不知道如何在 cmets 中正确格式化)。如果您能看一下,看看您是否可以解决错误,那就太好了? 感谢 Kirstein -> 做到了。它比使用 Objective C 复杂得多,但我感谢您的帮助。下次可能会直接看使用核心数据,不管数据量如何。使用 Swift 似乎有更少的简单解决方案...... 如果变量之一是对象,如何保存?所以如果我们有 var person = Person()

以上是关于Swift 中的简单持久存储的主要内容,如果未能解决你的问题,请参考以下文章

Core Data 和 Swift 中的计算属性与持久存储

在 Swift 中持久存储大量数据

OpenStack之Swift学习

Swift - 向 Core Data 添加启用 iCloud 的持久存储

Swift持久化存储对象到文件中JSONEncoder 与 JSONDecoder

Swift持久化存储对象到文件中JSONEncoder 与 JSONDecoder