Saturday, June 11, 2016

Downloading and parsing JSON data in Swift 2.2

I've primarily moved to the Android world, but I had a side gig that required me to get a jump start on Swift. Last time I touched Swift was with Swift 1.1, and things have changed a bit.

In this tutorial I'm going to show you how to download data from a JSON file stored somewhere on the web, then we're going to parse it, and finally we're going to show it in the UI of the app.

For this tutorial we're going to use NSURLSession and NSURLSessionDataTask to download the data, and we'll use the regular class NSJSONSerialization to parse the downloaded JSON data.
All 3 of these classes are part of the UIKit of iOS, and they are not third-party libraries.

Enough talk, let's get to work.
Note: this was done on June 2016, using Xcode 7.3.1 and Swift 2.2.

Sample JSON

We'll begin with a super simple JSON file that I have stored here, and in case the url is down by the time you read this, here's a copy of it's content:
{

    "name": "Eduardo Flores",
    "age": 32,
    "gender": "male",
    "country": "USA"

}
As you can see, this JSON is super simple. It only contains 1 JSON object (the root object), and 4 key/value pairs, where 3 of these values are Strings and 1 is an Int.

Download the data

For this project I'm going to assume that you know how to create an iOS project in Xcode, so I'll skip that part.
My project is a single view project with nothing on it.

We first need to define the url where our JSON file is, so we'll do that this way:
import UIKit
class ViewController: UIViewController
{

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let jsonUrlAsString = "https://api.myjson.com/bins/2c0aw"
    }
}
After this, we need to setup a configuration we're going to use.
While headers won't be really needed for this simple JSON, I will add them to this tutorial so you know where to place them. Headers go as a dictionary of key/value pairs.
Here's what my configuration, making a GET request, with headers looks like:
import UIKit
class ViewController: UIViewController
{

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let jsonUrlAsString = "https://api.myjson.com/bins/2c0aw"
        
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let headers: [NSObject : AnyObject] = ["Accept":"application/json"]
        configuration.HTTPAdditionalHeaders = headers
        let session = NSURLSession(configuration: configuration)
    }
}
If we had additional headers, like an authentication token or something else, that would look like this:
let headers: [NSObject : AnyObject] = ["Accept":"application/json","Auth":"token"]
So, with the configuration set, and the session variable created, we now need to create the NSURLSessionDataTask to actually make the network download.
For this we will use the NSURLSessionDataTask initializer that takes a NSURL, and a completion handler, like this:
session.dataTaskWithURL(NSURL:url>, completionHandler: <(NSData?, NSURLResponse?, NSError?) -> Void)
Since we want to use this, we need to assign this line to a variable.
The variable, with the completion handler using real variables, would then look like this:
let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
}
And in order to actually make the network call, we need to call the .resume() method of our downloadTask variable.
All together now, this looks like this:
import UIKit
class ViewController: UIViewController
{

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let jsonUrlAsString = "https://api.myjson.com/bins/2c0aw"
        
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let headers: [NSObject : AnyObject] = ["Accept":"application/json", "Auth":"token"]
        configuration.HTTPAdditionalHeaders = headers
        let session = NSURLSession(configuration: configuration)

        let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
        }

        // actually execute the task
        downloadTask.resume()
    }
}
Yay, you've made a GET request. Woohoo!
But nothing visible has happened yet...

Check the downloaded data

In our completion handler of session.dataTaskWithURL we have 3 elements: a NSData object, a NSURLResponse object, and an NSError object.
If something went wrong during the download of the data, our NSError object will have an error, and therefore it won't be nil. If this happens, the NSData and NSURLResponse objects will be nil.
In other words, if the NSError object is nil then the NSData and NSURLResponse objects have data, and if the NSError object is not nil, then the NSData and NSURLResponse objects will be nil.
So, let's check for errors.
let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
    if (error == nil)
    {
        print("dataReceived = \(dataReceived)")
    }
    else
    {
        print("Error downloading data. Error = \(error)")
    }
}
This is now inside the downloadTask variable, and all I'm doing here is checking to see if the NSError object is nil. If the NSError object is nil, then I print the downloaded data to the console.
Otherwise it means that the NSError is not nil, and something went wrong somewhere.
Also, since the NSError object is nil, the dataReceived variable (coming from the completion handler of session.dataTaskWithURL) should contain data.

If you run the app now, you should have something like this in the console:
dataReceived = Optional(<os_dispatch_data: buf="0x7fc2d14453a0" data="" leaf="" size="66," x7fc2d1604cd0="">)
This is ugly and unusable, but at least it shows you we're getting data back!

Convert the downloaded NSData to JSON object

Assuming our data downloads correctly, and therefore there are no errors at this point, we need to convert the dataReceived into a readable JSONObject. For this, we'll use the NSJSONSerialization class, like this:
NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
However, this may throw an exception, as most serializers do, and we want to place the result of the Serialization inside of a variable. (note: there are several options inside the NSJSONReadingOptions class if you want to read into this)

So, adding the exception catcher, in true Java try/catch fashion, we do this now:
let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
    if (error == nil)
    {
        print("dataReceived = \(dataReceived)")
        do
        {
            let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
            print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
        }
        catch
        {
            
        }
    }
    else
    {
        print("Error downloading data. Error = \(error)")
    }
}

// actually execute the task
downloadTask.resume()
And now, the output should be something much friendlier, like this:
dataReceived = Optional()
dataDownloadedAsJson = {
    age = 32;
    country = USA;
    gender = male;
    name = "Eduardo Flores";
}
Yay, we have our JSON file, with readable data in the console!

Parsing the JSON data

Now that we have our JSON data available, we need to create Swift variables so we can pass the data around our application. We begin this process by parsing the entire JSON file into small variables for whatever elements we want.

The first thing we're going to do is get the name key of our JSON object. Since this JSON file is super simple, this is a 1 liner, like this:
let nameRead = dataDownloadedAsJson["name"] as? String
print("nameRead = \(nameRead!)")
And when you run it, you should have this output:
name = Eduardo Flores
So, what does this do?
This is actually fairly simple.
1. We have all of our serialized JSON data in a variable called dataDownloadedAsJson
2. Inside our JSON object, we're looking for the key of name
3. We believe, or expect, the value of our key name to be a String object. We could've used the as! keyword (with the exclamation point) instead of as? (with the question mark), but it is preferred to use the question mark version, which allows us to receive nil values. The as! keyword is expecting a String value, while the as? allows String AND nil values. (this is called Optionals, in case you want to read more about it)
4. String would be expected object type
5. We assign the result to this to a new variable called nameRead
6. And when we display it to the console we use the nameRead! with the exclamation point to unwrap the object into a String value

With that, we create variables for all of the key/values from our JSON. Note that you don't need to parse every single element in your JSON and you could just parse the elements you need.

And with that, our entire application looks like this now:
import UIKit
class ViewController: UIViewController
{

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let jsonUrlAsString = "https://api.myjson.com/bins/2c0aw"
        
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let headers: [NSObject : AnyObject] = ["Accept":"application/json", "Auth":"token"]
        configuration.HTTPAdditionalHeaders = headers
        let session = NSURLSession(configuration: configuration)

        let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
            if (error == nil)
            {
                print("dataReceived = \(dataReceived)")
                do
                {
                    let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
                    print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
                    
                    let nameRead = dataDownloadedAsJson["name"] as? String
                    let countryRead = dataDownloadedAsJson["country"] as? String
                    let genderRead = dataDownloadedAsJson["gender"] as? String
                    let ageRead = dataDownloadedAsJson["age"] as? Int
                    
                    print("nameRead = \(nameRead!)")
                    print("countryRead = \(countryRead!)")
                    print("genderRead = \(genderRead!)")
                    print("ageRead = \(ageRead!)")
                }
                catch
                {
                    
                }
            }
            else
            {
                print("Error downloading data. Error = \(error)")
            }
        }
        
        // actually execute the task
        downloadTask.resume()
    }
}


Display data in UI

With the data downloaded and parsed, we now need to display our data in our UI.
For this I've created 4 UI elements in the storyboard, which I've wired up as IBOutlet in my ViewController file.

Control + Click to create connections

So now we could just try the regular self.something command we use to set elements, right?
Let's do it and see what happens. Here's the code of the downloadTask with the new code to set the UI elements:
let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
    if (error == nil)
    {
        print("dataReceived = \(dataReceived)")
        do
        {
            let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
            print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
            
            let nameRead = dataDownloadedAsJson["name"] as? String
            let countryRead = dataDownloadedAsJson["country"] as? String
            let genderRead = dataDownloadedAsJson["gender"] as? String
            let ageRead = dataDownloadedAsJson["age"] as? Int
            
            print("nameRead = \(nameRead!)")
            print("countryRead = \(countryRead!)")
            print("genderRead = \(genderRead!)")
            print("ageRead = \(ageRead!)")
            
            // set UI elements
            self.labelName.text = nameRead!
            self.labelAge.text = String(ageRead!)
            self.labelGender.text = genderRead!
            self.labelCountry.text = countryRead!
        }
        catch
        {
            
        }
    }
    else
    {
        print("Error downloading data. Error = \(error)")
    }
}
And run the app, and you'll get something like this:
Console output, and Simulator running
Your app runs, the console displays the correct output, but your app in the simulator never shows the correct values.
How is this possible, since we clearly have them in the console?

While we never explicitly requested this, the NSURLSessionDataTask class runs on a separate thread, which IS NOT THE UI THREAD.
This means that all of this code will run on the background, and someday, in the distant future, your UI will catch up and update with the code you run on the background thread.
This is a great feature of NSURLSessionDataTask because it allows us to make multiple network calls without locking up the UI for the user, but you need to be aware of it, and need to learn how to handle it properly (not just waiting forever for the UI to update).

So how do we solve this?
We call Apple's friendly (and C language looking) Grand Central Dispatch, and ask it to run our UI code in the UI thead, like this:
// set UI elements
// on the main thread
dispatch_async(dispatch_get_main_queue()) { () -> Void in
    self.labelName.text = nameRead!
    self.labelAge.text = String(ageRead!)
    self.labelGender.text = genderRead!
    self.labelCountry.text = countryRead!
}

So now, the entire code our of our entire application looks like this:
import UIKit
class ViewController: UIViewController
{

    @IBOutlet weak var labelName: UILabel!
    @IBOutlet weak var labelAge: UILabel!
    @IBOutlet weak var labelGender: UILabel!
    @IBOutlet weak var labelCountry: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let jsonUrlAsString = "https://api.myjson.com/bins/2c0aw"
        
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let headers: [NSObject : AnyObject] = ["Accept":"application/json", "Auth":"token"]
        configuration.HTTPAdditionalHeaders = headers
        let session = NSURLSession(configuration: configuration)

        let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
            if (error == nil)
            {
                print("dataReceived = \(dataReceived)")
                do
                {
                    let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
                    print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
                    
                    let nameRead = dataDownloadedAsJson["name"] as? String
                    let countryRead = dataDownloadedAsJson["country"] as? String
                    let genderRead = dataDownloadedAsJson["gender"] as? String
                    let ageRead = dataDownloadedAsJson["age"] as? Int
                    
                    print("nameRead = \(nameRead!)")
                    print("countryRead = \(countryRead!)")
                    print("genderRead = \(genderRead!)")
                    print("ageRead = \(ageRead!)")
                    
                    // set UI elements
                    // on the main thread
                    dispatch_async(dispatch_get_main_queue()) { () -> Void in
                        self.labelName.text = nameRead!
                        self.labelAge.text = String(ageRead!)
                        self.labelGender.text = genderRead!
                        self.labelCountry.text = countryRead!
                    }
                }
                catch
                {
                    
                }
            }
            else
            {
                print("Error downloading data. Error = \(error)")
            }
        }
        
        // actually execute the task
        downloadTask.resume()
    }
}

And there you have it folks!
Now you can run your app, and the UI will be updating as soon as the data gets downloaded.

On my next tutorial I will be showing you how to return the downloaded data to another class, using your own completion handler.
This is more the likely the pattern you'll be using to download data in a larger app.

Eduardo.

1 comment:

  1. Exelent tutorial and very helpful! Thanks
    Hope you continue doing well! :)

    ReplyDelete