Persisting Images to Disk

Each item’s itemKey is encoded and decoded, but what about its image? At the moment, images 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.

Listing 15.19  Adding a method to get a URL for a given image (ImageStore.swift)

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 Data buffer.

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

Listing 15.20  Saving image data to disk (ImageStore.swift)

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 = image.jpegData(compressionQuality: 0.5) {
        // Write it to full URL
        try? data.write(to: url)
    }
}

Let’s examine this code more closely. The jpegData(compressionQuality:) method takes a single parameter that determines the compression quality when converting the image to a JPEG data format. 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.

Finally, you call write(to:) to write the image data to the filesystem, as you did in Chapter 13.

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

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

Listing 15.21  Fetching the image from the filesystem if it is not in the cache (ImageStore.swift)

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, similar to an if statement but with some key differences. Unlike an if statement, a guard statement must have an else block that exits scope. A guard statement is used when some condition must be met in order for the code after it to be executed. If that condition is met, program execution continues past the guard statement, and any variables bound in the guard statement are available for use.

Here, the condition is whether the UIImage initialization is successful. If the initialization succeeds, imageFromDisk is available to use. If the initialization fails, the else block is executed, returning nil.

The code above is functionally equivalent to:

    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. Now you need the 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.

Listing 15.22  Removing the image from the filesystem (ImageStore.swift)

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

    let url = imageURL(forKey: key)
    do {
        try FileManager.default.removeItem(at: url)
    } catch {
        print("Error removing the image from disk: (error)")
    }
}

Build and run the application now that the ImageStore is complete. Select a photo for an item and exit the application to the Home screen. Launch the application again. Selecting that same item will show all its saved details – except the photo you just took.

You are successfully taking, showing, and saving images. But you are not yet loading images from the store when you need them. You will do that next.

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

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