16
Saving, Loading, and Application States

There are many ways to save and load data in an iOS application. This chapter will take you through some of the most common mechanisms as well as the concepts you need for writing to or reading from the filesystem in iOS. Along the way, you will be updating Homepwner so that its data persists between runs (Figure 16.1).

Figure 16.1  Homepwner in the task switcher

Screenshot shows the task switcher interface. The Homepwner application is shown at the center, and the Safari application and the app screen are shown on the left and right sides, respectively.

Archiving

Most iOS applications are, at base, doing the same thing: providing an interface for the user to manipulate data. Every object in an application has a role in this process. Model objects are responsible for holding on to the data that the user manipulates. View objects reflect that data, and controllers are responsible for keeping the views and the model objects in sync. Therefore, saving and loading data almost always means saving and loading model objects.

In Homepwner, the model objects that a user manipulates are instances of Item. For Homepwner to be a useful application, instances of Item must persist between runs of the application. You will be using archiving to save and load Item objects.

Archiving is one of the most common ways of persisting model objects in iOS. Archiving an object involves recording all of its properties and saving them to the filesystem. Unarchiving re-creates the object from that data.

Classes whose instances need to be archived and unarchived must conform to the NSCoding protocol and implement its two required methods, encode(with:) and init(coder:).

protocol NSCoding {
    func encode(with aCoder: NSCoder)
    init?(coder aDecoder: NSCoder)
}

When objects are added to an interface file, such as a storyboard file, they are archived. At runtime, the objects are loaded into memory by being unarchived from the interface file. UIView and UIViewController both conform to the NSCoding protocol, so both can be archived and unarchived without any extra effort from you.

Your Item class, on the other hand, does not currently conform to NSCoding. Open Homepwner.xcodeproj and add this protocol declaration in Item.swift.

class Item: NSObject, NSCoding {

The next step is to implement the required methods. Let’s start with encode(with:). When an Item is sent the message encode(with:), it will encode all of its properties into the NSCoder object that is passed as an argument. While saving, you will use NSCoder to write out a stream of data. That stream will be organized as key-value pairs and stored on the filesystem.

In Item.swift, implement encode(with:) to add the names and values of the item’s properties to the stream.

func encode(with aCoder: NSCoder) {
    aCoder.encode(name, forKey: "name")
    aCoder.encode(dateCreated, forKey: "dateCreated")
    aCoder.encode(itemKey, forKey: "itemKey")
    aCoder.encode(serialNumber, forKey: "serialNumber")

    aCoder.encode(valueInDollars, forKey: "valueInDollars")
}

To find out which encoding methods to use for other Swift types, you can check the documentation for NSCoder. Regardless of the type of the encoded value, there is always a key, which is a string that identifies which property is being encoded. By convention, this key is the name of the property being encoded.

Encoding is a recursive process. When an instance is encoded (that is, when it is the first argument in encode(_:forKey:)), that instance is sent encode(with:). During the execution of its encode(with:) method, it encodes its properties using encode(_:forKey:) (Figure 16.2). Thus, each instance encodes any properties that it references, which encode any properties that they reference, and so on.

Figure 16.2  Encoding an object

Illustration shows the encoding of an object.

The purpose of the key is to retrieve the encoded value when this Item is loaded from the filesystem later. Objects being loaded from an archive are sent the message init(coder:). This method should grab all of the objects that were encoded in encode(with:) and assign them to the appropriate property.

In Item.swift, implement init(coder:).

required init(coder aDecoder: NSCoder) {
    name = aDecoder.decodeObject(forKey: "name") as! String
    dateCreated = aDecoder.decodeObject(forKey: "dateCreated") as! Date
    itemKey = aDecoder.decodeObject(forKey: "itemKey") as! String
    serialNumber = aDecoder.decodeObject(forKey: "serialNumber") as! String?

    valueInDollars = aDecoder.decodeInteger(forKey: "valueInDollars")

    super.init()
}

Notice that this method has an NSCoder argument, too. In init(coder:), the NSCoder is full of data to be consumed by the Item being initialized. Also notice that you call decodeObject(forKey:) on the container to get objects and decodeInteger(forKey:) to get the valueInDollars.

In Chapter 10, we talked about the initializer chain and designated initializers. The init(coder:) method is not part of this design pattern. You will keep Item’s designated initializer the same, and init(coder:) will not call it.

Instances of Item are now NSCoding compliant and can be saved to and loaded from the filesystem using archiving. You can build the application now to make sure there are no syntax errors, but you do not yet have a way to kick off the saving and loading. You also need a place on the filesystem to store the saved items.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset