JSON Data

JSON data, especially when it is condensed like it is in your console, may seem daunting. However, it is actually a very simple syntax. JSON can contain the most basic types used to represent model objects: arrays, dictionaries, strings, and numbers. A JSON dictionary contains one or more key-value pairs, where the key is a string and the value can be another dictionary or a string, number, or array. An array can consist of strings, numbers, dictionaries, and other arrays. Thus, a JSON document is a nested set of these types of values.

Here is an example of some really simple JSON:

{
    "name" : "Christian",
    "friends" : ["Stacy", "Mikey"],
    "job" : {
        "company" : "Big Nerd Ranch",
        "title" : "Senior Nerd"
    }
}

This JSON document begins and ends with curly braces ({ and }), which in JSON delimit a dictionary. Within the curly braces are the key-value pairs that belong to the dictionary. This dictionary contains three key-value pairs (name, friends, and job).

A string is represented by text within quotation marks. Strings are used as the keys within a dictionary and can be used as values, too. Thus, the value associated with the name key in the top-level dictionary is the string Christian.

Arrays are represented with square brackets ([ and ]). An array can contain any other JSON information. In this case, the friends key holds an array of strings (Stacy and Mikey).

A dictionary can contain other dictionaries, and the final key in the top-level dictionary, job, is associated with a dictionary that has two key-value pairs (company and title).

Photorama will parse out the useful information from the JSON data and store it in a Photo instance.

JSONSerialization

Apple has a built-in class for parsing JSON data, JSONSerialization. You can hand this class a bunch of JSON data, and it will create a dictionary for every JSON dictionary (the JSON specification calls these objects), an array for every JSON array, a String for every JSON string, and an NSNumber for every JSON number. Let’s see how this class helps you.

Open PhotoStore.swift and update fetchInterestingPhotos() to print the JSON object to the console.

func fetchInterestingPhotos() {

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

        if let jsonData = data {
            if let jsonString = String(data: jsonData,
                                       encoding: .utf8) {
                print(jsonString)
            }
            do {
                let jsonObject = try JSONSerialization.jsonObject(with: jsonData,
                                                                  options: [])
                print(jsonObject)
            } catch let error {
                print("Error creating JSON object: (error)")
            }
        } else if let requestError = error {
            print("Error fetching interesting photos: (requestError)")
        } else {
            print("Unexpected error with the request")
        }
    }
    task.resume()
}

Build and run the application, then check the console. You will see the JSON data again, but now it will be formatted differently because print() does a good job formatting dictionaries and arrays.

The format of the JSON data is dictated by the API, so you will add the code to parse the JSON to the FlickrAPI struct.

Parsing the data that comes back from the server could go wrong in a number of ways: The data might not contain JSON. The data could be corrupt. The data might contain JSON but not match the format that you expect. To manage the possibility of failure, you will use an enumeration with associated values to represent the success or failure of the parsing.

Enumerations and associated values

You learned about the basics of enumerations in Chapter 2, and you have been using them throughout this book – including the Method enum used earlier in this chapter. Associated values are a useful feature of enumerations. Let’s take a moment to look at a simple example before you use this feature in Photorama.

Enumerations are a convenient way of defining and restricting the possible values for a variable. For example, let’s say you are working on a home automation app. You could define an enumeration to specify the oven state, like this:

enum OvenState {
    case on
    case off
}

If the oven is on, you also need to know what temperature it is set to. Associated values are a perfect solution to this situation.

enum OvenState {
    case on(Double)
    case off
}

var ovenState = OvenState.on(450)

Each case of an enumeration can have data of any type associated with it. For OvenState, its .on case has an associated Double that represents the oven’s temperature. Notice that not all cases need to have associated values.

Retrieving the associated value from an enum is often done using a switch statement.

switch ovenState {
case let .on(temperature):
    print("The oven is on and set to (temperature) degrees.")
case .off:
    print("The oven is off.")
}

Note that the .on case uses a let keyword to store the associated value in the temperature constant, which can be used within the case clause. (You can use the var keyword instead if temperature needs to be a variable.) Considering the value given to ovenState, the switch statement above would result in the line The oven is on and set to 450 degrees. printed to the console.

In the next section, you will use an enumeration with associated values to tie the result status of a request to the Flickr web service with data. A successful result status will be tied to the data containing interesting photos; a failure result status will be tied with error information.

Parsing JSON data

In PhotoStore.swift, add an enumeration named PhotosResult to the top of the file that has a case for both success and failure.

import Foundation

enum PhotosResult {
    case success([Photo])
    case failure(Error)
}

class PhotoStore {

If the data is valid JSON and contains an array of photos, those photos will be associated with the success case. If there are any errors during the parsing process, the relevant Error will be passed along with the failure case.

Error is a protocol that all errors conform to. NSError is the error that many iOS frameworks throw, and it conforms to Error. You will create your own Error shortly.

In FlickrAPI.swift, implement a method that takes in an instance of Data and uses the JSONSerialization class to convert the data into the basic foundation objects.

static func photos(fromJSON data: Data) -> PhotosResult {
    do {
        let jsonObject = try JSONSerialization.jsonObject(with: data,
                                                          options: [])

        var finalPhotos = [Photo]()
        return .success(finalPhotos)
    } catch let error {
        return .failure(error)
    }
}

(This code will generate some warnings. You will resolve them shortly.)

If the incoming data is valid JSON data, then the jsonObject instance will reference the appropriate model object. If not, then there was a problem with the data and you pass along the error. You now need to get the photo information out of the JSON object and into instances of Photo.

When the URLSessionDataTask finishes, you will use JSONSerialization to convert the JSON data into a dictionary. Figure 20.5 shows how the data will be structured.

Figure 20.5  JSON objects

Illustration shows the structure of data in JSON Objects.

At the top level of the incoming JSON data is a dictionary. The value associated with the photos key contains the important information, and the most important is the array of dictionaries.

As you can see, you have to dig pretty deep to get the information that you need.

If the structure of the JSON data does not match your expectations, you will return a custom error.

At the top of FlickrAPI.swift, declare a custom enum to represent possible errors for the Flickr API.

enum FlickrError: Error {
    case invalidJSONData
}

enum Method: String {
    case interestingPhotos = "flickr.interestingness.getList"
}

Now, in photos(fromJSON:), dig down through the JSON data to get to the array of dictionaries representing the individual photos.

static func photos(fromJSON data: Data) -> PhotosResult {
    do {
        let jsonObject = try JSONSerialization.jsonObject(with: data,
                                                          options: [])

        guard
            let jsonDictionary = jsonObject as? [AnyHashable:Any],
            let photos = jsonDictionary["photos"] as? [String:Any],
            let photosArray = photos["photo"] as? [[String:Any]] else {

                // The JSON structure doesn't match our expectations
                return .failure(FlickrError.invalidJSONData)
        }

        var finalPhotos = [Photo]()
        return .success(finalPhotos)
    } catch let error {
        return .failure(error)
    }
}

The next step is to get the photo information out of the dictionary and into Photo model objects.

You will need an instance of DateFormatter to convert the datetaken string into an instance of Date.

In FlickrAPI.swift, add a constant instance of DateFormatter.

private static let baseURLString = "https://api.flickr.com/services/rest"
private static let apiKey = "a6d819499131071f158fd740860a5a88"

private static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    return formatter
}()

Still in FlickrAPI.swift, write a new method to parse a JSON dictionary into a Photo instance.

private static func photo(fromJSON json: [String : Any]) -> Photo? {
    guard
        let photoID = json["id"] as? String,
        let title = json["title"] as? String,
        let dateString = json["datetaken"] as? String,
        let photoURLString = json["url_h"] as? String,
        let url = URL(string: photoURLString),
        let dateTaken = dateFormatter.date(from: dateString) else {

            // Don't have enough information to construct a Photo
            return nil
    }

    return Photo(title: title, photoID: photoID, remoteURL: url, dateTaken: dateTaken)
}

Now update photos(fromJSON:) to parse the dictionaries into Photo instances and then return these as part of the success enumerator. Also handle the possibility that the JSON format has changed, so no photos were able to be found.

static func photos(fromJSON data: Data) -> PhotosResult {
    do {
        let jsonObject = try JSONSerialization.jsonObject(with: data,
                                                          options: [])

        guard
            let jsonDictionary = jsonObject as? [AnyHashable:Any],
            let photos = jsonDictionary["photos"] as? [String:Any],
            let photosArray = photos["photo"] as? [[String:Any]] else {

                // The JSON structure doesn't match our expectations
                return .failure(FlickrError.invalidJSONData)
        }

        var finalPhotos = [Photo]()
        for photoJSON in photosArray {
            if let photo = photo(fromJSON: photoJSON) {
                finalPhotos.append(photo)
            }
        }

        if finalPhotos.isEmpty && !photosArray.isEmpty {
            // We weren't able to parse any of the photos
            // Maybe the JSON format for photos has changed
            return .failure(FlickrError.invalidJSONData)
        }
        return .success(finalPhotos)
    } catch let error {
        return .failure(error)
    }
}

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

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

    return FlickrAPI.photos(fromJSON: jsonData)
}

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

func fetchInterestingPhotos() {

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

        if let jsonData = data {
            do {
                let jsonObject = try JSONSerialization.jsonObject(with: jsonData,
                                                                  options: [])
                print(jsonObject)
            } catch let error {
                print("Error creating JSON object: (error)")
            }
        } 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.

func fetchInterestingPhotos(completion: @escaping (PhotosResult) -> Void) {

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

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

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 PhotosResult. 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.6 describes the flow of data with the web service request.

Figure 20.6  Web service request data flow

Illustration shows the flow of data with the web service request.

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 the viewDidLoad() using the trailing closure syntax to print out the result of the web service request.

override func viewDidLoad() {
    super.viewDidLoad()

    store.fetchInterestingPhotos() {
        (photosResult) -> Void 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. Once the web service request completes, you should see the number of photos found printed to the console.

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

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