Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simple persistent storage in Swift

Tags:

I have an array of objects each with a number of properties. Here is some sample data taken by looping through the array of objects:

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

Its very little data so I want to avoid using core data. I need to be able to persist save the array then open it again and be able to loop through it. I was hoping to use a simple solution like NSUserDefaults or NSKeyedArchiver but in Swift I can get neither to work with this type of array (I've been going through documentation and forums and examples online for 24 hours now...)

How would you recommend I persist save an array of objects like the above? OR maybe persist saving this type of array is bad practice?

Thanks in advance for your help!

Adding the object class:

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
    }

}

If I try to save the array to NSUserDefaults I'm getting the error:

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

I have tried using inheriting from the NSCoder class but I get an error which Im not able to resolve, as below:

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

    }
}
like image 491
user3535074 Avatar asked Oct 07 '14 09:10

user3535074


People also ask

What is persistent store in Swift?

A persistent store is a repository in which managed objects may be stored. You can think of a persistent store as a database data file where individual records each hold the last-saved values of a managed object.


2 Answers

One possibility is to convert your object properties:values to string:object store them to NSUserDefaults and then get and decode them back.

If you want to store your object using NSKeyedArchiver, your class needs to conform to NSCoding and be subclass of NSObject. Example:

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

    }
}

Then you can archive and save to 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]
like image 94
Kirsteins Avatar answered Oct 22 '22 22:10

Kirsteins


Core Data is incredibly powerful and I highly recommend you try not be swayed by its intimidating appearance. When your app grows you will be incredibly grateful that you backed your data with Core Data as it scales incredibly well.

That being said, theres a nice article on NSHipster that covers the basics of using the NSKeyedArchiver. So the solution would be to have your objects be a subclass of NSObject and conform to the NSCoding protocol. This allows you to archive an unarchive the objects from the disk. You can the save your file to the documents directory

Your subclass would then need to implicitly unwrap all the encode-able variables, the result would yield:

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

You could then add a way of finding the location to the documents directory:

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

So archiving would look like this:

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

and later on you could read your array back from disk:

let receipts = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath)

I would recommend against using User Defaults to store an array, its meant to store preferences, here take a look at the documentation:

The NSUserDefaults class provides a programmatic interface for interacting with the defaults system. The defaults system allows an application to customize its behavior to match a user’s preferences. For example, you can allow users to determine what units of measurement your application displays or how often documents are automatically saved. Applications record such preferences by assigning values to a set of parameters in a user’s defaults database.

Ideally you don't want to persist arrays because then you have to fetch the entire array into memory even if you need one object. Naturally Core Data will solve all that unnecessary heap mess for you:)

EDIT

To ease yourself into Core Data you could always look at Magical Record. They simplify a lot of the cruft needed to maintain the Core Data stack.

Good Luck

Core Data Evangelist

like image 21
Daniel Galasko Avatar answered Oct 23 '22 00:10

Daniel Galasko