Chapter 4
Working with JSON and Codable in Swift
First, what's JSON? JSON (short for JavaScript Object Notation) is a text-based, lightweight, and easy way for storing and exchanging data. It's commonly used for representing structural data and data interchange in client-server applications, serving as an alternative to XML. A lot of the web services we use every day have JSON-based APIs. Most of the iOS apps, including Twitter, Facebook, and Flickr send data to their backend web services in JSON format.
As an example, here is a JSON representation of a sample Movie object:
{
"title": "The Amazing Spider-man",
"release_date": "03/07/2012",
"director": "Marc Webb",
"cast": [
{
"name": "Andrew Garfield",
"character": "Peter Parker"
},
{
"name": "Emma Stone",
"character": "Gwen Stacy"
},
{
"name": "Rhys Ifans",
"character": "Dr. Curt Connors"
}
]
}
As you can see, JSON formatted data is more human-readable and easier to parse than XML. I'll not go into the details of JSON. This is not the purpose of this chapter. If you want to learn more about the technology, I recommend you to check out the JSON Guide at http://www.json.org/.
Since the release of iOS 5, the iOS SDK has already made it easy for developers to fetch and parse JSON data. It comes with a handy class called NSJSONSerialization
, which can automatically convert JSON formatted data to objects. Later in this chapter, I will show you how to use the API to parse some sample JSON formatted data, returned by a web service. Once you understand how it works, it is fairly easy to build an app by integrating with other free/paid web services.
Since the release of Swift 4, Apple introduced the Codable
protocol to simplify the whole JSON archival and serialization process. We will also look into this new feature and see how we can apply it in JSON parsing.
Demo App
As usual, we'll create a demo app. Let's call it KivaLoan. The reason why we name the app KivaLoan is that we will utilize a JSON-based API provided by Kiva.org. If you haven't heard of Kiva, it is a non-profit organization with a mission to connect people through lending to alleviate poverty. It lets individuals lend as little as $25 to help create opportunities around the world. Kiva provides free web-based APIs for developers to access their data. For our demo app, we'll call up the following Kiva API to retrieve the most recent fundraising loans and display them in a table view:
https://api.kivaws.org/v1/loans/newest.json
Quick note: Starting from iOS 9, Apple introduced a feature called App Transport Security (ATS) with the aim to improve the security of connections between an app and web services. By default, all outgoing connections should ride on HTTPS. Otherwise, your app will not be allowed to connect to the web service. Optionally, you can add a key namedNSAllowsArbitraryLoads
in the Info.plist and set the value to YES to disable ATS, so that you can connect to web APIs over HTTP. However, you'll have to take note if you useNSAllowsArbitraryLoads
in your apps. In iOS 10, Apple further enforces ATS for all iOS apps. By January 2017, all iOS apps should be ATS-compliant. In other words, if your app connects to external any web services, the connection must be over HTTPS. If your app can't fulfil this requirement, Apple will not allow it to be released on the App Store.
The returned data of the above API is in JSON format. Here is a sample result:
loans: (
{
activity = Retail;
"basket_amount" = 0;
"bonus_credit_eligibility" = 0;
"borrower_count" = 1;
description = {
languages = (
fr,
en
);
};
"funded_amount" = 0;
id = 734117;
image = {
id = 1641389;
"template_id" = 1;
};
"lender_count" = 0;
"loan_amount" = 750;
location = {
country = Senegal;
"country_code" = SN;
geo = {
level = country;
pairs = "14 -14";
type = point;
};
};
name = "Mar\U00e8me";
"partner_id" = 108;
"planned_expiration_date" = "2016-08-05T09:20:02Z";
"posted_date" = "2016-07-06T09:20:02Z";
sector = Retail;
status = fundraising;
use = "to buy fabric to resell";
},
....
....
)
You will learn how to use the NSJSONSerialization
class to convert the JSON formatted data into objects. It's unbelievably simple. You'll see what I mean in a while.
To keep you focused on learning the JSON implementation, you can first download the project template from http://www.appcoda.com/resources/swift59/KivaLoanStarter.zip. I have already created the skeleton of the app for you. It is a simple table-based app that displays a list of loans provided by Kiva.org. The project template includes a pre-built storyboard and custom classes for the table view controller and prototype cell. If you run the template, it should result in an empty table app.
Creating JSON Data Model
We will first create a class to model a loan. It's not required for loading JSON but the best practice is to create a separate class (or structure) for storing the data model. The Loan
class represents the loan information in the KivaLoan app and is used to store the loan information returned by Kiva.org. To keep things simple, we won't use all the returned data of a loan. Instead, the app will just display the following fields of a loan:
- Name of the loan applicant
name = "Mar\U00e8me";
- Country of the loan applicant
location = {
country = Senegal;
"country_code" = SN;
geo = {
level = country;
pairs = "14 -14";
type = point;
};
};
- How the loan will be used
use = "to buy fabric to resell";
- Amount
"loan_amount" = 750;
These fields are good enough for filling up the labels in the table view. Now create a new class file using the Swift File template. Name it Loan.swift
and declare the Loan
structure like this:
struct Loan: Hashable {
var name: String = ""
var country: String = ""
var use: String = ""
var amount: Int = 0
}
JSON supports a few basic data types including number, String, Boolean, Array, and Objects (an associated array with key and value pairs).
For the loan fields, the loan amount is stored as a numeric value in the JSON-formatted data. This is why we declared the amount
property with the type Int
. For the rest of the fields, they are declared with the type String
.
Fetching Loans with the Kiva API
As I mentioned earlier, the Kiva API is free to use. No registration is required. You may point your browser to the following URL and you'll get the latest fundraising loans in JSON format.
https://api.kivaws.org/v1/loans/newest.json
Okay, let's see how we can call up the Kiva API and parse the returned data. First, open KivaLoanTableViewController.swift
and declare two variables at the very beginning:
private let kivaLoanURL = "https://api.kivaws.org/v1/loans/newest.json"
private var loans = [Loan]()
We just defined the URL of the Kiva API, and declare the loans
variable for storing an array of Loan objects. Next, insert the following methods in the same file:
func getLatestLoans() {
guard let loanUrl = URL(string: kivaLoanURL) else {
return
}
let request = URLRequest(url: loanUrl)
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if let error = error {
print(error)
return
}
// Parse JSON data
if let data = data {
self.loans = self.parseJsonData(data: data)
// Update table view's data
OperationQueue.main.addOperation({
self.updateSnapshot()
})
}
})
task.resume()
}
func parseJsonData(data: Data) -> [Loan] {
var loans = [Loan]()
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
// Parse JSON data
let jsonLoans = jsonResult?["loans"] as! [AnyObject]
for jsonLoan in jsonLoans {
var loan = Loan()
loan.name = jsonLoan["name"] as! String
loan.amount = jsonLoan["loan_amount"] as! Int
loan.use = jsonLoan["use"] as! String
let location = jsonLoan["location"] as! [String:AnyObject]
loan.country = location["country"] as! String
loans.append(loan)
}
} catch {
print(error)
}
return loans
}
These two methods form the core part of the app. Both methods work collaboratively to call the Kiva API, retrieve the latest loans in JSON format and translate the JSON-formatted data into an array of Loan
objects. Let's go through them in detail.
In the getLatestLoans
method, we first instantiate the URL
structure with the URL of the Kiva Loan API. The initialization returns us an optional. This is why we use the guard
keyword to see if the optional has a value. If not, we simply return and skip all the code in the method.
Next, we create a URLSession
with the load URL. The URLSession
class provides APIs for dealing with online content over HTTP and HTTPS. The shared session is good enough for making simple HTTP/HTTPS requests. In case you have to support your own networking protocol, URLSession
also provides you an option to create a custom session.
One great thing of URLSession
is that you can add a series of session tasks to handle the loading of data, as well as uploading and downloading files and data fetching from servers (e.g. JSON data fetching).
With sessions, you can schedule three types of tasks: data tasks (URLSessionDataTask
) for retrieving data to memory, download tasks (URLSessionDownloadTask
) for downloading a file to disk, and upload tasks (URLSessionUploadTask
) for uploading a file from disk. Here we use the data task to retrieve contents from Kiva.org. To add a data task to the session, we call the dataTask
method with the specific URL request. After you add the task, the session will not take any action. You have to call the resume method (i.e. task.resume()
) to initiate the data task.
Like most networking APIs, the URLSession
API is asynchronous. Once the request completes, it returns the data (as well as errors) by calling the completion handler.
In the completion handler, immediately after the data is returned, we check for an error. If no error is found, we invoke the parseJsonData
method.
The data returned is in JSON format. We create a helper method called parseJsonData
for converting the given JSON-formatted data into an array of Loan
objects. The Foundation framework provides the JSONSerialization
class, which is capable of converting JSON to Foundation objects and converting Foundation objects to JSON. In the code snippet, we call the jsonObject
method with the given JSON data to perform the conversion.
When converting JSON formatted data to objects, the top-level item is usually converted to a Dictionary or an Array. In this case, the top level of the returned data of the Kiva API is converted to a dictionary. You can access the array of loans using the key loans
.
How do you know what key to use?
You can either refer to the API documentation or test the JSON data using a JSON browser (e.g. http://jsonviewer.stack.hu). If you've loaded the Kiva API into the JSON browser, here is an excerpt from the result:
{
"paging": {
"page": 1,
"total": 5297,
"page_size": 20,
"pages": 265
},
"loans": [
{
"id": 794429,
"name": "Joel",
"description": {
"languages": [
"es",
"en"
]
},
"status": "fundraising",
"funded_amount": 0,
"basket_amount": 0,
"image": {
"id": 1729143,
"template_id": 1
},
"activity": "Home Appliances",
"sector": "Personal Use",
"use": "To buy home appliances.",
"location": {
"country_code": "PE",
"country": "Peru",
"town": "Ica",
"geo": {
"level": "country",
"pairs": "-10 -76",
"type": "point"
}
},
"partner_id": 139,
"posted_date": "2015-11-20T08:50:02Z",
"planned_expiration_date": "2016-01-04T08:50:02Z",
"loan_amount": 400,
"borrower_count": 1,
"lender_count": 0,
"bonus_credit_eligibility": true,
"tags": [
]
},
{
"id": 797222,
"name": "Lucy",
"description": {
"languages": [
"en"
]
},
"status": "fundraising",
"funded_amount": 0,
"basket_amount": 0,
"image": {
"id": 1732818,
"template_id": 1
},
"activity": "Farm Supplies",
"sector": "Agriculture",
"use": "To purchase a biogas system for clean cooking",
"location": {
"country_code": "KE",
"country": "Kenya",
"town": "Gatitu",
"geo": {
"level": "country",
"pairs": "1 38",
"type": "point"
}
},
"partner_id": 436,
"posted_date": "2016-11-20T08:50:02Z",
"planned_expiration_date": "2016-01-04T08:50:02Z",
"loan_amount": 800,
"borrower_count": 1,
"lender_count": 0,
"bonus_credit_eligibility": false,
"tags": [
]
},
...
As you can see from the above code, paging
and loans
are two of the top-level items. Once the JSONSerialization
class converts the JSON data, the result (i.e. jsonResult
) is returned as a Dictionary with the top-level items as keys. This is why we can use the key loans
to access the array of loans. Here is the line of code for your reference:
let jsonLoans = jsonResult?["loans"] as! [AnyObject]
With the array of loans (i.e. jsonLoans) returned, we loop through the array. Each of the array items (i.e. jsonLoan) is converted into a dictionary. In the loop, we extract the loan data from each of the dictionaries and save them in a Loan
object. Again, you can find the keys (highlighted in yellow) by studying the JSON result. The value of a particular result is stored as AnyObject
. AnyObject
is used because a JSON value could be a String, Double, Boolean, Array, Dictionary or null. This is why you have to downcast the value to a specific type such as String
and Int
. Lastly, we put the loan
object into the loans
array, which is the return value of the method.
for jsonLoan in jsonLoans {
var loan = Loan()
loan.name = jsonLoan["name"] as! String
loan.amount = jsonLoan["loan_amount"] as! Int
loan.use = jsonLoan["use"] as! String
let location = jsonLoan["location"] as! [String: AnyObject]
loan.country = location["country"] as! String
loans.append(loan)
}
After the JSON data is parsed and the array of loans is returned, we call the reloadData
method to reload the table. You may wonder why we need to call OperationQueue.main.addOperation
and execute the data reload in the main thread.
The block of code in the completion handler of the data task is executed in a background thread. If you call the updateSnapshot
method in the background thread, the data reload will not happen immediately. To ensure a responsive GUI update, this operation should be performed in the main thread. This is why we call the OperationQueue.main.addOperation
method and request to run the updateSnapshot
method in the main queue.
OperationQueue.main.addOperation({
self.updateSnapshot()
})
Quick note: You can also usedispatch_async
function to execute a block of code in the main thread. But according to Apple, it is recommended to useOperationQueue
overdispatch_async
. As a general rule, Apple recommends using the highest-level APIs rather than dropping down to the low-level ones.
We haven't implemented the updateSnapshot()
method yet, which will be discussed in the next section.
Displaying Loans in A Table View
With the loans
array in place, the last thing we need to do is to display the data in the table view. First, declare a Section
enum in KivaLoanTableViewController
:
enum Section {
case all
}
Next, insert the following methods in the same class:
func configureDataSource() -> UITableViewDiffableDataSource<Section, Loan> {
let cellIdentifier = "Cell"
let dataSource = UITableViewDiffableDataSource<Section, Loan>(
tableView: tableView,
cellProvider: { tableView, indexPath, loan in
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! KivaLoanTableViewCell
cell.nameLabel.text = loan.name
cell.countryLabel.text = loan.country
cell.useLabel.text = loan.use
cell.amountLabel.text = "$\(loan.amount)"
return cell
}
)
return dataSource
}
func updateSnapshot(animatingChange: Bool = false) {
// Create a snapshot and populate the data
var snapshot = NSDiffableDataSourceSnapshot<Section, Loan>()
snapshot.appendSections([.all])
snapshot.appendItems(loans, toSection: .all)
dataSource.apply(snapshot, animatingDifferences: animatingChange)
}
The above code is pretty straightforward if you understand how to implement table view using UITableViewDiffableDataSource
, which is now the recommended approach to populate a table view's data. If you are new to UITableViewDiffableDataSource
, please refer to our beginner book.
In brief, the configureDataSource
method is written to retrieve the loan information from the loans
array and populate them in the custom table cell. One thing to take note of is the code below:
"$\(loan.amount)"
In some cases, you may want to create a string by adding both string (e.g. $) and integer (e.g. "$\(loan.amount)"
) together. Swift provides a powerful way to create these kinds of strings, known as string interpolation. You can make use of it by using the above syntax.
The updateSnapshot
method is designed to populate the loans into the table.
Next, declare a dataSource
variable like this:
lazy var dataSource = configureDataSource()
Lastly, insert the following line of code in the viewDidLoad
method to start fetching the loan data:
getLatestLoans()
Compile and Run the App
Now it's time to test the app. Compile and run it in the simulator. Once launched, the app will pull the latest loans from Kiva.org and display them in the table view.
Introducing Codable
Starting from Swift 4, Apple introduced a new way to encode and decode JSON data using Codable
. We will rewrite the JSON decoding part of the demo app using this new approach.
Before we jump right into the modification, let me give you a basic walkthrough of Codable
. If you look into the documentation of Codable
, it is just a type alias of a protocol composition:
typealias Codable = Decodable & Encodable
Decodable
and Encodable
are the two actual protocols you need to work with. However, for convenience's sake, we usually refer to this type alias for handling JSON encoding and decoding.
To continue reading and access the full version of the book, please get the full copy here. You will also be able to access the full source code of the project.