Passing the Photos Around

Let’s finish parsing the photos. Open FlickrAPI.swift and implement a method that takes in an instance of Data and uses the JSONDecoder class to convert the data into an instance of FlickrResponse.

Listing 20.21  Decoding the JSON data (FlickrAPI.swift)

static func photos(fromJSON data: Data) -> Result<[Photo], Error> {
    do {
        let decoder = JSONDecoder()
        let flickrResponse = try decoder.decode(FlickrResponse.self, from: data)
        return .success(flickrResponse.photosInfo.photos)
    } catch {
        return .failure(error)
    }
}

If the incoming data is structured JSON in the form expected, then it will be parsed successfully, and the flickrResponse instance will be set. If there is a problem with the data, an error will be thrown, which you catch and pass along.

Notice that this new method returns a Result type. Result is an enumeration defined within the Swift standard library that is useful for encapsulating the result of an operation that might succeed or fail. Result has two cases, success and failure, and each of these cases has an associated value that represents the successful value and error, respectively:

    public enum Result<Success, Failure> where Failure : Error {

        /// A success, storing a `Success` value.
        case success(Success)

        /// A failure, storing a `Failure` value.
        case failure(Failure)
    }

Unlike most of the types you have worked with, Result is a generic type, which means that it uses placeholder types that are defined when you use it. For Result, there are two placeholders that you define: what kind of value it should contain on success and what kind of value it should contain on failure. Notice the where clause at the end of the first line; this limits the failure associated value to be some kind of Error.

To fill in these generic placeholders, you specify the values when using the type by enclosing them within the angled brackets, as you did in Result<[Photo], Error>. This defines a Result where the success case is associated with an array of photos, and the failure case is associated with any Error.

Incidentally, Array is another generic type you have used. Its placeholder type defines what kind of elements will exist within the array. As you saw in Chapter 2, you can use the angled bracket syntax (Array<String>), but it is much more common to use the shorthand notation ([String]).

Next, in PhotoStore.swift, write a new method that will process the JSON data that is returned from the web service request.

Listing 20.22  Processing the web service data (PhotoStore.swift)

private func processPhotosRequest(data: Data?,
                                  error: Error?) -> Result<[Photo], Error> {
    guard let jsonData = data else {
        return .failure(error!)
    }

    return FlickrAPI.photos(fromJSON: jsonData)
}

Now, update fetchInterestingPhotos() to use the method you just created.

Listing 20.23  Factoring out the data parsing code (PhotoStore.swift)

func fetchInterestingPhotos() {

    let url = FlickrAPI.interestingPhotosURL
    let request = URLRequest(url: url)
    let task = session.dataTask(with: request) {
        (data, response, error) in

        if let jsonData = data {
            if let jsonString = String(data: jsonData,
                                       encoding: .utf8) {
                print(jsonString)
            }
        } else if let requestError = error {
            print("Error fetching interesting photos: (requestError)")
        } else {
            print("Unexpected error with the request")
        }

        let result = self.processPhotosRequest(data: data, error: error)
    }
    task.resume()
}

Finally, update the method signature for fetchInterestingPhotos() to take in a completion closure that will be called once the web service request is completed.

Listing 20.24  Adding a completion handler (PhotoStore.swift)

func fetchInterestingPhotos(completion: @escaping (Result<[Photo], Error>) -> Void) {

    let url = FlickrAPI.interestingPhotosURL
    let request = URLRequest(url: url)
    let task = session.dataTask(with: request) {
        (data, response, error) in

        let result = self.processPhotosRequest(data: data, error: error)
        completion(result)
    }
    task.resume()
}

The completion closure takes in a Result instance and returns nothing. But to indicate that this is a closure, you need to specify the return type – so you specify Void (in other words, no return type). Without -> Void, the compiler would assume that the completion parameter takes in a Result instance instead of a closure.

Fetching data from a web service is an asynchronous process: Once the request starts, it may take a nontrivial amount of time for a response to come back from the server. Because of this, the fetchInterestingPhotos(completion:) method cannot directly return an instance of Result<[Photo], Error>. Instead, the caller of this method will supply a completion closure for the PhotoStore to call once the request is complete.

This follows the same pattern that URLSessionTask uses with its completion handler: The task is created with a closure for it to call once the web service request completes. Figure 20.5 describes the flow of data with the web service request.

Figure 20.5  Web service request data flow

Web service request data flow

The closure is marked with the @escaping annotation. This annotation lets the compiler know that the closure might not get called immediately within the method. In this case, the closure is getting passed to the URLSessionDataTask, which will call it when the web service request completes.

In PhotosViewController.swift, update the implementation of viewDidLoad() using the trailing closure syntax to print out the result of the web service request.

Listing 20.25  Printing the results of the request (PhotosViewController.swift)

override func viewDidLoad() {
    super.viewDidLoad()

    store.fetchInterestingPhotos()
    store.fetchInterestingPhotos {
        (photosResult) in

        switch photosResult {
        case let .success(photos):
            print("Successfully found (photos.count) photos.")
        case let .failure(error):
            print("Error fetching interesting photos: (error)")
        }

    }
}

Build and run the application. Take a look at the console, and you will notice an error message printed out. (We have broken the error onto multiple lines due to page length constraints.)

    Error fetching interesting photos: typeMismatch(Swift.Double 1,
        Swift.DecodingError.Context(codingPath: 2
        [CodingKeys(stringValue: "photos" 3, intValue: nil),
        CodingKeys(stringValue: "photo" 4, intValue: nil),
        _JSONKey(stringValue: "Index 0", intValue: 0) 5,
        CodingKeys(stringValue: "datetaken" 6, intValue: nil)],
        debugDescription: "Expected to decode Double but found a
        string/data instead." 7, underlyingError: nil))

There is a lot to parse here, but it is important to be able to understand these error messages. Let’s break this down piece by piece.

1

Indicates that the decoder was expecting a Double but received something different.

2

Shows the path to the key within the JSON structure that triggered the error.

3

Indicates that the problematic key is within the photos key object.

4

Indicates that, within the photos object, the problematic key is within the photo key object.

5

Informs us that photo is an array, and the issue is related to the object at index 0 within that array.

6

Shows the final part of the coding path array, indicating that the problem is with the datetaken key.

7

Describes the reason for the type mismatch, to give you context.

The error message says that the datetaken key is triggering the error, and the debugDescription tells you that the reason for the error is that the decoder expected to get a Double from the JSON, but it found a String or Data instead.

If you take a look at the JSON output that the server sends, you will notice that the date is formatted like this:

    2019-07-25 15:06:30

This is the string that the debug description is referring to. By default, JSONDecoder expects JSON dates to be represented as a time interval from the reference date (00:00:00 UTC on 1 January 2001), which is expressed as a Double. This explains the type mismatch error you are currently experiencing.

To address this issue, you can provide JSONDecoder a custom date decoding strategy.

Open FlickrAPI.swift and update photos(fromJSON:) to use a custom date decoding strategy.

Listing 20.26  Adding a custom date decoding strategy (FlickrAPI.swift)

static func photos(fromJSON data: Data) -> Result<[Photo], Error> {
    do {
        let decoder = JSONDecoder()

        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        decoder.dateDecodingStrategy = .formatted(dateFormatter)

        let flickrResponse = try decoder.decode(FlickrResponse.self, from: data)
        return .success(flickrResponse.photosInfo.photos)
    } catch let error {
        return .failure(error)
    }
}

There are a few built-in date decoding strategies, but Flickr’s date format does not follow any of the these. (Flickr’s API says it sends dates in the MySQL ‘datetime’ format.) Because of this, you create a custom date formatter, setting the date format to what the Flickr API sends. This date is sent in the Greenwich Mean Time (GMT) time zone, so to accurately represent the date you set the locale and timeZone on the date formatter. Finally, you use this date formatter to assign a custom formatted date decoding strategy to the decoder.

Build and run again. Look at the console, and you may notice another error:

    Error fetching recent photos:
        keyNotFound(CodingKeys(stringValue: "url_z", intValue: nil),
        Swift.DecodingError.Context(codingPath:
        [CodingKeys(stringValue: "photos", intValue: nil),
        CodingKeys(stringValue: "photo", intValue: nil),
        _JSONKey(stringValue: "Index 13", intValue: 13)],
        debugDescription: "No value associated with key CodingKeys
            (stringValue: "url_z", intValue: nil) ("url_z").",
            underlyingError: nil))

Thankfully this error is easier to address. Looking at the debug description, you will notice that one of the photo objects did not have a "url_z" key associated with it. In the example above, the _JSONKey line mentions index 13, implying that the previous photos were decoded successfully but that the photo at index 13 failed.

Flickr photos can have multiple URLs that are associated with different sizes, and not every photo will have every size. (If you did not get an error message, it is because every photo that came back from your web service request had a "url_z" key associated with it.)

Since Photo has non-optional properties, JSONDecoder requires these properties to be in the JSON data, or the decoding fails. To address the issue, you need to mark the remoteURL property (which is your custom property name for url_z) as optional.

Open Photo.swift and change the remoteURL property from non-optional to optional.

Listing 20.27  Making the remoteURL optional (Photo.swift)

class Photo: Codable {
    let title: String
    let remoteURL: URL
    let remoteURL: URL?
    let photoID: String
    let dateTaken: Date

    enum CodingKeys: String, CodingKey {
        case title
        case remoteURL = "url_z"
        case photoID = "id"
        case dateTaken = "datetaken"
    }
}

Build and run again, and you should now see the photo parsing successfully. The console should print something like Successfully found 93 photos.

No error is good, but you do not want to work with photos that do not have a URL. Open FlickrAPI.swift and update photos(fromJSON:) to remove any photos missing a URL.

Listing 20.28  Filtering out photos with a missing URL (FlickrAPI.swift)

static func photos(fromJSON data: Data) -> Result<[Photo], Error> {
    do {
        let decoder = JSONDecoder()

        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
        decoder.dateDecodingStrategy = .formatted(dateFormatter)

        let flickrResponse = try decoder.decode(FlickrResponse.self, from: data)
        return .success(flickrResponse.photosInfo.photos)

        let photos = flickrResponse.photosInfo.photos.filter { $0.remoteURL != nil }
        return .success(photos)
    } catch let error {
        return .failure(error)
    }
}

The filter(_:) method acts on an array and generates a new array. It takes in a closure that determines whether each element in the original array should be included in the new array. The closure gets called on each element and returns a Boolean indicating whether that element should be included in the new array. Here, you are including each Photo that has a remoteURL that is not nil.

Build and run the app. After the web service request completes, you should again see the app successfully parsing some number of photos.

With that complete, turn your attention to downloading the image data associated with the photos.

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

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