NEW: Master Swift design patterns with my latest book! >>

< Previous: Creating the basic UI: UITabBarController   Next: Rendering a petition: loadHTMLString >

Parsing JSON: Loading data into SwiftyJSON

JSON – short for JavaScript Object Notation – is a way of describing data. It's not the easiest to read yourself, but it's compact and easy to parse for computers, which makes it popular online where bandwidth is at a premium.

In project 6 you learned about using dictionaries with Auto Layout, and in this project we're going to use dictionaries more extensively. What's more, we're going to put dictionaries inside an array to make an array of dictionaries, which should keep our data in order.

You declare a dictionary using square brackets, then entering its key type, a colon, and its value type. For example, a dictionary that used strings for its keys and UILabels for its values would be declared like this:

var labels = [String: UILabel]()

And as you'll recall, you declare arrays just by putting the data type in brackets, like this:

var petitions = [String]()

Putting these two together, we want to make an array of dictionaries, with each dictionary holding a string for its key and another string for its value. So, it looks like this:

var petitions = [[String: String]]()

Put that in place of the current petitions definition at the top of ViewController.swift.

It's now time to parse some JSON, which means to process it and examine its contents. This isn't easy in Swift, so a number of helper libraries have appeared that do a lot of the heavy lifting for you. We're going to use one of them now: download the files for this project from GitHub then look for a file called SwiftyJSON.swift. Add that your project, making sure "Copy items if needed" is checked.

SwiftyJSON lets us read through JSON in a natural way: you can effectively treat almost everything as a dictionary, so if you know there's a value called "information" that contains another value called "name", which in turns contains another value called "firstName", you can use json["information"]["name"]["firstName"] to get the data, then ask for it as a Swift value by using the string property.

Before we do the parsing, here is a tiny slice of the actual JSON you'll be receiving:

{
    "metadata":{
        "responseInfo":{
            "status":200,
            "developerMessage":"OK",
        }
    },
    "results":[
        {
            "title":"Legal immigrants should get freedom before undocumented immigrants – moral, just and fair",
            "body":"I am petitioning President Trump's Administration to take a humane view of the plight of legal immigrants. Specifically, legal immigrants in Employment Based (EB) category. I believe, such immigrants were short changed in the recently announced reforms via Executive Action (EA), which was otherwise long due and a welcome announcement.",
            "issues":[
                {
                    "id":"28",
                    "name":"Human Rights"
                },
                {
                    "id":"29",
                    "name":"Immigration"
                }
            ],
            "signatureThreshold":100000,
            "signatureCount":267,
            "signaturesNeeded":99733,
        },
        {
            "title":"National database for police shootings.",
            "body":"There is no reliable national data on how many people are shot by police officers each year. In signing this petition, I am urging the President to bring an end to this absence of visibility by creating a federally controlled, publicly accessible database of officer-involved shootings.",
            "issues":[
                {
                    "id":"28",
                    "name":"Human Rights"
                }
            ],
            "signatureThreshold":100000,
            "signatureCount":17453,
            "signaturesNeeded":82547,
        }
    ]
}

You'll actually be getting between 2000-3000 lines of that stuff, all containing petitions from US citizens about all sorts of political things. It doesn't really matter (to us) what the petitions are, we just care about the data structure. In particular:

  1. There's a metadata value, which contains a responseInfo value, which in turn contains a status value. Status 200 is what internet developers use for "everything is OK."
  2. There's a results value, which contains a series of petitions.
  3. Each petition contains a title, a body, some issues it relates to, plus some signature information.
  4. JSON has strings and integers too. Notice how the strings are all wrapped in quotes, whereas the integers aren't.

Now that you have a basic understanding of the JSON we'll be working with, it's time to write some code. We're going to update the viewDidLoad() method for ViewController so that it downloads the data from the Whitehouse petitions server, converts it to a SwiftyJSON object, and checks that the status value is equal to 200.

You already saw that String can be created using contentsOfFile to load data from disk. Well, String can also be created using contentsOf, which downloads data from a URL (specified using URL) and makes it available to you.

This is perfect for our needs – here's the new viewDidLoad method:

override func viewDidLoad() {
    super.viewDidLoad()

    let urlString = "https://api.whitehouse.gov/v1/petitions.json?limit=100"

    if let url = URL(string: urlString) {
        if let data = try? String(contentsOf: url) {
            let json = JSON(parseJSON: data)

            if json["metadata"]["responseInfo"]["status"].intValue == 200 {
                // we're OK to parse!
            }
        }
    }
}

Let's focus on the new stuff:

  • urlString points to the Whitehouse.gov server, accessing the petitions system.
  • We use if let to make sure the URL is valid, rather than force unwrapping it. Later on you can return to this to add more URLs, so it's good play it safe.
  • We create a new String object using its contentsOf method. This returns the content from a URL, but it might throw an error (i.e., if the internet connection was down) so we need to use try?.
  • If the String object was created successfully, we create a new JSON object from it. This is a SwiftyJSON structure.
  • Finally, we have our first bit of JSON parsing: if there is a "metadata" value and it contains a "responseInfo" value that contains a "status" value, return it as an integer, then compare it to 200.
  • The "we're OK to parse!" line starts with //, which begins a comment line in Swift. Comment lines are ignored by the compiler; we write them as notes to ourselves.

The reason SwiftyJSON is so good at JSON parsing is because it has optionality built into its core. If any of "metadata", "responseInfo" or "status" don't exist, this call will return 0 for the status – we don't need to check them all individually. If you're reading a string value, SwiftyJSON will return either the string it found, or if it didn't exist then an empty string.

This code isn't perfect, in fact far from it. In fact, by downloading data from the internet in viewDidLoad() our app will lock up until all the data has been transferred. There are solutions to this, but to avoid complexity they won't be covered until project 9.

For now, we want to focus on our JSON parsing. We already have a petitions array that is ready to accept dictionaries of data. We want to parse that JSON into dictionaries, with each dictionary having three values: the title of the petition, its body text, and how many signatures it has. Once that's done, we need to tell our table view to reload itself.

Are you ready? Because this code is remarkably simple given how much work it's doing:

func parse(json: JSON) {
    for result in json["results"].arrayValue {
        let title = result["title"].stringValue
        let body = result["body"].stringValue
        let sigs = result["signatureCount"].stringValue
        let obj = ["title": title, "body": body, "sigs": sigs]
        petitions.append(obj)
    }

    tableView.reloadData()
}

Place that method just underneath viewDidLoad() method, then replace the existing // we're OK to parse! line in viewDidLoad() with this:

parse(json: json)

The parse() method reads the "results" array from the JSON object it gets passed. If you look back at the JSON snippet I showed you, that results array contains all the petitions ready to read. When you use arrayValue with SwiftyJSON, you either get back an array of objects or an empty array – both of those are good to use for our loop.

For each result in the results array, we read out three values: its title, its body, and its signature count, with all three of them being requested as strings. The signature count is actually a number when it comes in the JSON, but SwiftyJSON converts it for us so we can put it inside our dictionary where all the keys and values are strings.

Each time we're accessing an item in our result value using stringValue, we will either get its value back or an empty string. Regardless, we'll have something, so we construct a new dictionary from all three values then use petitions.append() to place the new dictionary into our array.

Once all the results have been parsed, we tell the table view to reload, and the code is complete.

You can run the program now, although it just shows “Title goes here” and “Subtitle goes here” again and again, because our cellForRowAt method just inserts dummy data.

We want to modify this so that the cells print out the title value of our dictionary, but we also want to use the subtitle text label that got added when we changed the cell type from "Basic" to "Subtitle" in the storyboard. To do that, change the cellForRowAt method to this:

let petition = petitions[indexPath.row]
cell.textLabel?.text = petition["title"]
cell.detailTextLabel?.text = petition["body"]

We set the title, body and sigs keys in the dictionary, and now we can read them out to configure our cell correctly.

If you run the app now, you'll see things are starting to come together quite nicely – every table row now shows the petition title, and beneath it shows the first few words of the petition's body. The subtitle automatically shows "…" at the end when there isn't enough room for all the text, but it's enough to give the user a flavor of what's going on.

Get the ultimate experience

The Swift Power Pack includes my first six books for one low price, helping you jumpstart a new career in iOS development – check it out!

< Previous: Creating the basic UI: UITabBarController   Next: Rendering a petition: loadHTMLString >
Click here to visit the Hacking with Swift store >>