Writing to the Filesystem with Data

Your archiving in Homepwner saves and loads the itemKey for each Item, but what about the images? At the moment, they are lost when the app enters the background state. In this section, you will extend the image store to save images as they are added and fetch them as they are needed.

The images for Item instances should also be stored in the Documents directory. You can use the image key generated when the user takes a picture to name the image in the filesystem.

Implement a new method in ImageStore.swift named imageURL(forKey:) to create a URL in the documents directory using a given key.

func imageURL(forKey key: String) -> URL {

    let documentsDirectories =
        FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let documentDirectory = documentsDirectories.first!

    return documentDirectory.appendingPathComponent(key)
}

To save and load an image, you are going to copy the JPEG representation of the image into a buffer in memory. Instead of just creating a buffer, Swift programmers have a handy class to create, maintain, and destroy these sorts of buffers – Data. A Data instance holds some number of bytes of binary data, and you will use Data to store image data.

In ImageStore.swift, modify setImage(_:forKey:) to get a URL and save the image.

func setImage(_ image: UIImage, forKey key: String) {
    cache.setObject(image, forKey: key as NSString)

    // Create full URL for image
    let url = imageURL(forKey: key)

    // Turn image into JPEG data
    if let data = UIImageJPEGRepresentation(image, 0.5) {
        // Write it to full URL
        let _ = try? data.write(to: url, options: [.atomic])
    }
}

Let’s examine this code more closely. The function UIImageJPEGRepresentation takes two parameters: a UIImage and a compression quality. The compression quality is a Float from 0 to 1, where 1 is the highest quality (least compression). The function returns an instance of Data if the compression succeeds and nil if it does not.

This Data instance can be written to the filesystem by calling write(to:options:). The bytes held in the Data are then written to the URL specified by the first parameter. The second parameter allows for some options to be passed into the method. If the .atomic option is present, the file is written to a temporary place on the filesystem, and, once the writing operation is complete, that file is renamed to the URL of the first parameter, replacing any previously existing file. Writing atomically prevents data corruption should your application crash during the write procedure.

It is worth noting that this way of writing data to the filesystem is not archiving. While Data instances can be archived, using the method write(to:options:) copies the bytes in the Data directly to the filesystem.

Now that the image is stored in the filesystem, the ImageStore will need to load that image when it is requested. The initializer init(contentsOfFile:) of UIImage will read in an image from a file, given a URL.

In ImageStore.swift, update the method image(forKey:) so that the ImageStore will load the image from the filesystem if it does not already have it.

func image(forKey key: String) -> UIImage? {
    return cache.object(forKey: key as NSString)

    if let existingImage = cache.object(forKey: key as NSString) {
        return existingImage
    }

    let url = imageURL(forKey: key)
    guard let imageFromDisk = UIImage(contentsOfFile: url.path) else {
        return nil
    }

    cache.setObject(imageFromDisk, forKey: key as NSString)
    return imageFromDisk
}

What is that guard statement? guard is a conditional statement, like an if statement. The compiler will only continue past the guard statement if the condition within the guard is true. Here, the condition is whether the UIImage initialization is successful. If the initialization fails, the else block is executed, which allows you to have an early return. If the initialization succeeds, any variables or constants bound in the guard statement (here, imageFromDisk) are usable after the guard statement.

The code above is functionally equivalent to the following code:

if let imageFromDisk = UIImage(contentsOfFile: url.path) {
    cache.setObject(imageFromDisk, forKey: key)
    return imageFromDisk
}

return nil

While you could do this, guard provides both a cleaner – and, more importantly, a safer – way to ensure that you exit if you do not have what you need. Using guard also forces the failure case to be directly tied to the condition being checked. This makes the code more readable and easier to reason about.

You are able to save an image to disk and retrieve an image from disk, so the last thing you need to do is add functionality to remove an image from disk.

In ImageStore.swift, make sure that when an image is deleted from the store, it is also deleted from the filesystem. (You will see an error when you type in this code, which we will discuss next.)

func deleteImage(forKey key: String) {
    cache.removeObject(forKey: key as NSString)

    let url = imageURL(forKey: key)
    FileManager.default.removeItem(at: url)
}

Let’s take a look at the error message that this code generated, shown in Figure 16.8.

Figure 16.8  Error when removing the image from disk

An error is indicated in a line inside the function deleteImage.

This error message is letting you know that the method removeItem(at:) can fail, but you are not handling the error. Let’s fix this.

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

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