Codementor Events

Making a Bookkeeping App with NSUserDefaults and Complex Objects

Published Apr 28, 2015Last updated Jan 18, 2017
Making a Bookkeeping App with NSUserDefaults and Complex Objects

Persisting Complex Objects

In my previous article,, we covered the basics of how to persist data using NSUserDefaults.

Now we'll see how to persist custom classes that we create. These classes will be contained in an array. We'll save the entire array to NSUserDefaults.

Bookkeeping App

In this article, we'll create a simple register app that tracks monetary transactions. The user will enter either a positive or negative value. There will be an amount and description. The app will open scene 1 with a tableview of transactions. The user can click to display a second scene for entering in transactions.

A transaction class will be creating to hold the description and amount.

Building The App

To create the app, start a new project with a Single View Application. Set the language as Swift and Device as iPhone. Open the Main.storyboard and change the viewcontroller to a 4.7 iPhone from the Identity Inspector.

enter image description here

Since we are only creating for the iPhone, we want a viewcontroller size that is more realistic than the default square.

Click the view and delete it. From the Object Library, drag a tableview onto the viewcontroller. Your document outline should look like the following:

enter image description here

We can now add our second scene. From the Object Library, drag in a viewcontroller and size it to iPhone 4.7. Now add a second viewcontroller. Simply drag one from the Object Library onto the storyboard.

This app will be navigation controller based. There will be a navigation bar at the top of each scene. To create the navigation controller, click on the first view controller and in the top menu, click Editor > Embed In > Navigation Controller. Your story board will then look like the following:

enter image description here

It doesn't matter what the first navigation controller looks like. It won't be seen. In the above screenshot, there is an arrow going from the navigation controller to the first scene. We don't yet have an arrow to our second scene so there is no way to reach it. We'll fix that now.

Segueing And Navigation

To create a transition from scene 1 to scene 2, add a Bar Button Item to scene 1's navigation bar. Then set its style to Add.

enter image description here

Control click on the Add button and drag to the view on scene 2. This will create an Action Segue popup. Select show.

enter image description here

This will create a segue arrow between scene 1 and scene 2.

enter image description here

Let's give each view controller a name. In scene 1, click the navigation item and provide a name in the Identity Inspector. You can see below that I've chosen "Bookkeeper":

enter image description here

For scene 2, we need to add some navigation related components. In the Object Library, type in "navigation". Select Navigation Item and drag it into the navigation section of the view controller. Provide a title for the Navigation Item, just as we did for scene 1. I've gone with "Add Transaction".

enter image description here

If you run the app, you should be able to click the "+" and transition from scene 1 to scene 2.

Transaction Class Structure

We can now build out our Transaction class. Right click on the bookkeeper folder in the Project Navigator and select New. Choose Cocoa Touch class. Provide a name of Transaction and leave subclass as NSObject.

We need to provide an init method. This means we'll also have to override the original init method. Type in the following to do this:

init(description: String, amount: String) {
    super.init()
    self.transDescription = description
    self.amount = amount
}

This allows creation of the Transaction class using syntax such as:

var mytrans =  Transaction(description: "trans 1", amount: "$4.50")

We'll always need to provide values to our init method. That shouldn't be a problem in this simple app, since it is a very controlled environment. The only place we'll be creating transactions is in the add scene.

We can create two properties for our description and amount. We can't actually use description as a property since it is being used by NSObject. Instead, we'll use transDescription. Our amount will be a string since we aren't doing any computations on it.

var transDescription: String?
var amount: String?

Transaction Class Encoding/Decoding

There are two key components for serializing (i.e., saving) complex objects with NSUserDefaults. One is to adding encoding/decoding to the class that we're saving and the other is archiving and unarchiving calls using NSUserDefaults.

We're going to work on the encoding/decoding part first. The Transaction class needs to use the NSCoding protocol. Add the NSCoding protocol to your class declaration:

class Transaction: NSObject, NSCoding {

Now we need to encode/decode our class properties. The following code will do this:

required init(coder aDecoder: NSCoder) {
    if let transDescriptionDecoded = aDecoder.decodeObjectForKey("transDescription") as? String {
        self.transDescription = transDescriptionDecoded
    }
    if let amountEncoded = aDecoder.decodeObjectForKey("amount") as? String {
        self.amount = amountEncoded
    }
}
    
func encodeWithCoder(aCoder: NSCoder) {
    if let transDescriptionEncoded = self.transDescription {
        aCoder.encodeObject(transDescriptionEncoded, forKey: "transDescription")
    }
    if let amountEncoded = self.amount {
        aCoder.encodeObject(amountEncoded, forKey: "amount")
    }
}

Let's discuss this code. There are two methods:

required init(coder aDecoder: NSCoder) 

and

func encodeWithCoder(aCoder: NSCoder)

Both of these method signatures are following the NSCoding protocol. From their names, you can tell what they are doing. init decodes the properties, which is used when we retrieve them from NSUserDefaults. encodeWithCoder will encode the data, which we do when saving to NSUserDefaults.

Let's examine the inner workings of the init method, which will also describe the encode method since they are very similar.

The following looks up our property as a string parameter. This string is just a key that is associated to the actual property value.

if let transDescriptionDecoded = aDecoder.decodeObjectForKey("transDescription") as? String {
    self.transDescription = transDescriptionDecoded
}

The property value is then assigned to transDescriptionDecoded only if there is a value. That's why the optional ("?") is used. It is unwrapping the value (i.e. checks if there’s a value stored and takes that value). Only if there is a value do we go into the conditional and assign that value to our class property. The same is then done for the amount property.

Referencing The Array Of Transactions

In order to reference our array of transactions throughout the app, we're going to create a static class to hold them.

Add another new file to the project. It will again be a Cocoa Touch Class with the name TransactionManager and type is NSObject. Add the following single line to declare a static array of type Transaction:

class TransactionManager: NSObject {
    static var transactions = [Transaction]()
}

Now we can easily access the array of Transaction instances anywhere in the app with one line of code:

TransactionManager.transactions

Populating The Tableview

Before we can begin populating the Tableview, we need to modify our scene 1 viewcontroller. We're going to change it into a UITableview.

Change the class type from UIViewController to UITableViewController. We need to adhere to the UITableViewController protocols. Add the following two methods. I have also filled these in with the necessary code to retrieve values from the Transaction array.

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("mycell") as! UITableViewCell
    var transaction = TransactionManager.transactions[indexPath.item]
    cell.textLabel!.text = "\(transaction.transDescription) [\(transaction.amount))"
    return cell
}
    
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
     return TransactionManager.transactions.count
}

Once you begin typing tableview, hints should appear. Click enter when you see one of the above and the hint will fill out the code.

In

cellForRowAtIndexPath

we dequeue a tablecell and reuse it. Then we access the transaction array using the index value from indexPath.item. The value coming out of the array is assigned to a transaction variable. The values from this transaction are assigned to the textfield of the cell. Finally, we return the cell back to the tableview for display.

numberOfRowsInSection

simply returns the Transaction array count.

We can now populate the Transaction array with stub data. Add the following to scene 1's viewDidLoad() method:

var array = [
    Transaction(description: "trans 1", amount: "$4.50"),
    Transaction(description: "trans 2", amount: "$24.00"),
    Transaction(description: "trans 3", amount: "-$11.00"),
]
TransactionManager.transactions = array

This stub data will go away once scene 2 is wired up and the user can enter in data.

Wiring Up The Tableview

If we run the app now, we still won't see anything in the tableview. There are four things we must do first for the tableview:

  • Add a datasource
  • Add a delegate reference
  • Create an IBOutlet for the tableview
  • Supply a cell identifier that matches the one in
cellForRowAtIndexPath

The first three items on this list will be done in Interface Builder. Since we aren't using a prototype cell, the last must be done in code.

Let's go back to our Main.storyboard. In scene 1, click the tableview. In the Connections Inspector, drag from datasource to the tableview. Do the same for delegate.

enter image description here

Once completed, your outlets should look like the following:

enter image description here

Now we can create the tableview IBOutlet. Open the Assistant Editor. It should open to the ViewController class associated with scene 1. Control drag from the tableview to the top of viewDidLoad() in the ViewController class. This will create an IBOutlet popup. Supply the name tableview.

enter image description here

The final step is to tell the tableview what our cell identifier string is. This must be done in code since we aren't using a prototype tableview cell. In viewDidLoad(), add the following above the array code:

tableview.registerClass(UITableViewCell.self, forCellReuseIdentifier: "mycell")

You can see we're using the tableview IBOutlet that was just created. That's why we saved this part for last.

If you run the app, it should look like the following:

enter image description here

To remove the word Optional, use forced unwrapping for each value:

cell.textLabel!.text = "\(transaction.transDescription!) [\(transaction.amount!))"

Notice the bang ("!"). We are telling Swift that these values are known and not nil. If you run the app now, the Optional keyword will no longer be there.

Add Transactions Scene

Our add view doesn't have an associated class, which means there isn't anywhere for us to put our code for this view. We can add a class now. Add a new Cocoa Touch Class file. Give it the name AddViewController and subclass of UIViewController.

enter image description here

Go back into the storyboard. Click the AddView ViewController icon in the scene and open the Identity Inspector. Choose the AddViewController class.

enter image description here

We now have a place to type in our AddView code. We need to create a few IBOutlets for our fields and button. Control click drag from each input field to the AddViewController class, creating two IBOutlets with the names transactionDescription and amount.

@IBOutlet var transactionDescription: UITextField!
@IBOutlet var amount: UITextField!

Just like before, we can't use the keyword description so we make a subtle change to the name.

Create an IBAction for the button and called it saveButton_click:

enter image description here

Saving Data

All of the UI is built out at this point. We can save our data into the TransactionManager's array and then save this array into NSUserDefaults through the save button.

Add the following to the saveButton_click function body:

var transaction = Transaction(description: transactionDescription.text, amount: amount.text)
TransactionManager.transactions.append(transaction)
        
let defaults = NSUserDefaults.standardUserDefaults()
let myData = NSKeyedArchiver.archivedDataWithRootObject(TransactionManager.transactions)
NSUserDefaults.standardUserDefaults().setObject(myData, forKey: "transactionsarray")
defaults.synchronize()

The first thing that happens is we create a Transaction instance using the two input fields on the add view. Next, we initialize the NSUserDefaults singleton. To avoid typing this code again, we assign the singleton instance to a constant.

NSKeyedArchiver.archivedDataWithRootObject() takes our array and sets it up for NSUserDefaults compatibility. Then we take this formatted data and save it to NSUserDefaults. NSUserDefaults is basically a dictionary so we provide a key value of transactionsarray, which is the same key we'll use to retrieve our data later on.

Reading Saved Data

To read back data from NSUserDefaults, in ViewController, remove the temporary array code we added to viewDidLoad(). Then add the following to the viewDidLoad():

if let transactionsRaw = NSUserDefaults.standardUserDefaults().dataForKey("transactionsarray") {
    if let transactions = NSKeyedUnarchiver.unarchiveObjectWithData(transactionsRaw) as? [Transaction] {
    TransactionManager.transactions = transactions
    }
}

Let's walk through this code. The first line retrieves our value from NSUserDefaults by supplying the key transactionsarray. If the value is not nil, we take the raw value and transform it into usable data, casting it to a Transaction array. This array is then assigned to our TransactionManager array, where it is now available through out the application.

As a final touch to ensure we always have the latest data in our tableview after the user adds a transaction, we're going to reload data into the tableview each time scene 1 is displays:

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    tableView.reloadData()
}

Now you should be able to add an entry and have it immediately display in the tableview. If you shutdown the app, next time it loads, the array of transactions will be retrieved from NSUserDefaults and displayed, persisting the user's data from one app session to another.

Summary

In this article, we saw how to make a custom class serializable, allowing it to be saved in NSUserDefaults. This was done using the NSCoding protocols. Serializing this data means it is available from one app session to the other, creating data integrity for our app and providing a great user experience.

Discover and read more posts from Brett Romero
get started