Downloading the Image Data

Now all that is left is downloading the image data for the photos that come back in the request. This task is not very difficult, but it requires some thought. Images are large files, and downloading them could eat up your users’ cellular data allowance. As a considerate iOS developer, you want to make sure your app’s data usage is only what it needs to be.

Consider your options. You could download the image data in viewDidLoad() when the fetchInterestingPhotos(completion:) method calls its completion closure. At that point, you already assign the incoming photos to the photos property, so you could iterate over all of those photos and download their image data then.

Although this would work, it would be very costly. There could be a large number of photos coming back in the initial request, and the user may never even scroll down in the application far enough to see some of them. On top of that, if you initialize too many requests simultaneously, some of the requests may time out while waiting for other requests to finish. So this is probably not the best solution.

Instead, it makes sense to download the image data for only the cells that the user is attempting to view. UICollectionView has a mechanism to support this through its UICollectionViewDelegate method collectionView(_:willDisplay:forItemAt:). This delegate method will be called every time a cell is getting displayed onscreen and is a great opportunity to download the image data.

Recall that the data for the collection view is driven by an instance of PhotoDataSource, a reusable class with the single responsibility of displaying photos in a collection view. Collection views also have a delegate, which is responsible for handling user interaction with the collection view. This includes tasks such as managing cell selection and tracking cells coming into and out of view. This responsibility is more tightly coupled with the view controller itself, so whereas the data source is an instance of PhotoDataSource, the collection view’s delegate will be the PhotosViewController.

In PhotosViewController.swift, have the class conform to the UICollectionViewDelegate protocol.

class PhotosViewController: UIViewController, UICollectionViewDelegate {

(Because the UICollectionViewDelegate protocol only defines optional methods, Xcode does not report any errors when you add this declaration.)

Update viewDidLoad() to set the PhotosViewController as the delegate of the collection view.

override func viewDidLoad() {
    super.viewDidLoad()

    collectionView.dataSource = photoDataSource
    collectionView.delegate = self

Finally, implement the delegate method in PhotosViewController.swift.

func collectionView(_ collectionView: UICollectionView,
                    willDisplay cell: UICollectionViewCell,
                    forItemAt indexPath: IndexPath) {

    let photo = photoDataSource.photos[indexPath.row]

    // Download the image data, which could take some time
    store.fetchImage(for: photo) { (result) -> Void in

        // The index path for the photo might have changed between the
        // time the request started and finished, so find the most
        // recent index path

        // (Note: You will have an error on the next line; you will fix it soon)
        guard let photoIndex = self.photoDataSource.photos.index(of: photo),
            case let .success(image) = result else {
                return
        }
        let photoIndexPath = IndexPath(item: photoIndex, section: 0)

        // When the request finishes, only update the cell if it's still visible
        if let cell = self.collectionView.cellForItem(at: photoIndexPath)
                                                     as? PhotoCollectionViewCell {
            cell.update(with: image)
        }
    }
}

You are using a new form of pattern matching in the above code. The result that is returned from fetchImage(for:completion:) is an enumeration with two cases: .success and .failure. Because you only need to handle the .success case, you use a case statement to check whether result has a value of .success. Compare the following code to see how you could use pattern matching in an if statement versus a switch statement.

This code:

if case let .success(image) = result {
    photo.image = image
}

behaves just like this code:

switch result {
case let .success(image):
    photo.image = image
case .failure:
    break
}

Let’s fix the error you saw when finding the index of photo in the photos array. The index(of:) method works by comparing the item that you are looking for to each of the items in the collection. It does this using the == operator. Types that conform to the Equatable protocol must implement this operator, and Photo does not yet conform to Equatable.

In Photo.swift, declare that Photo conforms to the Equatable protocol and implement the required overloading of the == operator.

class Photo: Equatable {
    ...
    static func == (lhs: Photo, rhs: Photo) -> Bool {
        // Two Photos are the same if they have the same photoID
        return lhs.photoID == rhs.photoID
    }
}

In Swift, it is common to group related chunks of functionality into an extension. Let’s take a short detour to learn about extensions and then use this knowledge to see how conforming to the Equatable protocol is often done in practice.

Extensions

Extensions serve a couple of purposes: They allow you to group chunks of functionality into a logical unit, and they also allow you to add functionality to your own types as well as types provided by the system or other frameworks. Being able to add functionality to a type whose source code you do not have access to is a very powerful and flexible tool. Extensions can be added to classes, structs, and enums. Let’s take a look at an example.

Say you wanted to add functionality to the Int type to provide a doubled value of that Int. For example:

let fourteen = 7.doubled // The value of fourteen is '14'

You can add this functionality by extending the Int type:

extension Int {
    var doubled: Int {
        return self * 2
    }
}

With extensions, you can add computed properties, add methods, and conform to protocols. However, you cannot add stored properties to an extension.

Extensions provide a great mechanism for grouping related pieces of functionality. They can make the code more readable and help with long-term maintainability of your code base. One common chunk of functionality that is often grouped into an extension is conformance to a protocol along with the methods of that protocol.

Update Photo.swift to use an extension to conform to the Equatable protocol.

class Photo: Equatable {
    ...
    static func == (lhs: Photo, rhs: Photo) -> Bool {
        // Two Photos are the same if they have the same photoID
        return lhs.photoID == rhs.photoID
    }
}

extension Photo: Equatable {
    static func == (lhs: Photo, rhs: Photo) -> Bool {
        // Two Photos are the same if they have the same photoID
        return lhs.photoID == rhs.photoID
    }
}

This is a simplified example, but extensions are very powerful for both extending existing types and grouping related functionality. In fact, the Swift standard library makes extensive use of extensions – and you will, too.

Build and run the application. The image data will download for the cells visible onscreen (Figure 21.13). Scroll down to make more cells visible. At first, you will see the activity indicator views spinning, but soon the image data for those cells will load.

Figure 21.13  Image downloads in progress

Thumbnails of several photographs are shown in the Photorama interface. Some thumbnails are seen with a spinning activity indicator that implies that download is still in progress.

If you scroll back up, you will see a delay in loading the image data for the previously visible cells. This is because whenever a cell comes onscreen, the image data is redownloaded. To fix this, you will implement image caching, similar to what you did in the Homepwner application.

Image caching

For the image data, you will use the same approach that you used in your Homepwner application. In fact, you will use the same ImageStore class that you wrote for that project.

Open Homepwner.xcodeproj and drag the ImageStore.swift file from the Homepwner application to the Photorama application. Make sure to choose Copy items if needed. Once the ImageStore.swift file has been added to Photorama, you can close the Homepwner project.

Back in Photorama, open PhotoStore.swift and give it a property for an ImageStore.

class PhotoStore {

    let imageStore = ImageStore()

Then update fetchImage(for:completion:) to save the images using the imageStore.

func fetchImage(for photo: Photo, completion: @escaping (ImageResult) -> Void) {

    let photoKey = photo.photoID
    if let image = imageStore.image(forKey: photoKey) {
        OperationQueue.main.addOperation {
            completion(.success(image))
        }
        return
    }

    let photoURL = photo.remoteURL
    let request = URLRequest(url: photoURL)

    let task = session.dataTask(with: request) {
        (data, response, error) -> Void in

        let result = self.processImageRequest(data: data, error: error)

        if case let .success(image) = result {
            self.imageStore.setImage(image, forKey: photoKey)
        }

        OperationQueue.main.addOperation {
            completion(result)
        }
    }
    task.resume()
}

Build and run the application. Now when the image data is downloaded, it will be saved to the filesystem. The next time that photo is requested, it will be loaded from the filesystem if it is not currently in memory.

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

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