Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Swift Tutorial: Working with JSON

$
0
0

JSON tutorial

Update 12/21/2015: Updated for Xcode 7.1 and Swift 2.0.

JavaScript Object Notation, or JSON for short, is a common way to transmit data to and from web services. It is simple to use and human-readable, which is why it’s so incredibly popular.

Consider the following JSON snippet:

[
  {
    "person": {
      "name": "Dani",
      "age": "24"
    }
  },
  {
    "person": {
      "name": "ray",
      "age": "70"
    }
  }
]

In Objective-C, parsing and deserializing JSON is fairly straightforward:

NSArray *json = [NSJSONSerialization JSONObjectWithData:JSONData options:kNilOptions error:nil];
NSString *age = json[0][@"person"][@"age"];
NSLog(@"Dani's age is %@", age);

In Swift, parsing and deserializing JSON is a little more tedious due to Swift optionals and type-safety:

var json: Array!
do {
  json = try NSJSONSerialization.JSONObjectWithData(JSONData, options: NSJSONReadingOptions()) as? Array
} catch {
  print(error)
}
 
if let item = json[0] as? [String: AnyObject] {
  if let person = item["person"] as? [String: AnyObject] {
    if let age = person["age"] as? Int {
      print("Dani's age is \(age)")
    }
  }
}

In the code above, each object from the JSON must be checked before you use it via optional binding. This makes your code safe; but the more complicated your JSON is, the more ugly your code becomes.

As part of Swift 2.0 the guard statement was introduced to help get rid of nested if statements:

guard let item = json[0] as? [String: AnyObject],
  let person = item["person"] as? [String: AnyObject],
  let age = person["age"] as? Int else {
    return;
}
print("Dani's age is \(age)")

Still too verbose, isn’t it? But how could you make it simpler?

In this JSON tutorial, you’re going to learn an easier way to parse JSON in Swift – using a popular open source library called Gloss.

In particular, you’ll use Gloss to parse and map a JSON document containing the top 25 apps in the US App Store. You’ll see that it can be just as easy as it is in Objective-C! :]

Note: This tutorial assumes that you have some basic knowledge of Swift. If you are new to Swift, check out our Swift Quick Start tutorial series.

Getting Started

Download the starter playground for this tutorial.

Since a user interface isn’t important for this JSON tutorial, you’ll be working exclusively with playgrounds.

Open Swift.playground in Xcode and have a look around.

Note: You may find that the playground Project Navigator is closed by default. If so, simply type Command-1 to display it.

The starter playground file has a few source and resource files included to keep the focus purely on parsing JSON in Swift. Take a look at it’s structure to get an overview of what’s included:

  • The Resources folder bundles resources that can be accessed from your Swift code.
    • topapps.json: Contains the JSON string to be parsed.
  • The Sources folder contains additional Swift source files that your main playground code can access. Putting extra supporting .swift files into the Sources folder makes it easy to keep your playground clean and readable.
    • App.swift: A plain old Swift struct representing an app. Your goal is to parse the JSON into a collection of these objects.
    • DataManager.swift: Manages the data retrieval whether local or from the network. You’ll use the methods in this file to load some JSON later in this tutorial.

Once you feel like you have a good understanding of what’s currently in the playground, read on!

Parsing JSON the Native Swift Way

First, you’ll start by parsing JSON the native Swift way – i.e. without using any external libraries. This will help you understand the benefit of using a library like Gloss.

Note: If you already know about the pain of parsing JSON the native way and want to jump straight to Gloss, skip to the next section.

Let’s parse the provided JSON file to get the name of the #1 app on the App Store!

First, while you are going to work with dictionaries a lot, define a type alias at the top of the playground:

typealias Payload = [String: AnyObject]

Complete the code inside the callback of getTopAppsDataFromFileWithSuccess as follows:

DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in
 
  var json: Payload!
 
  // 1
  do {
    json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? Payload
  } catch {
    print(error)
    XCPlaygroundPage.currentPage.finishExecution()
  }
 
  // 2
  guard let feed = json["feed"] as? Payload,
    let apps = feed["entry"] as? [AnyObject],
    let app = apps.first as? Payload
    else { XCPlaygroundPage.currentPage.finishExecution() }
 
  guard let container = app["im:name"] as? Payload,
    let name = container["label"] as? String
    else { XCPlaygroundPage.currentPage.finishExecution() }
 
  guard let id = app["id"] as? Payload,
    let link = id["label"] as? String
    else { XCPlaygroundPage.currentPage.finishExecution() }
 
  // 3
  let entry = App(name: name, link: link)
  print(entry)
 
  XCPlaygroundPage.currentPage.finishExecution()
 
}

Here’s what’s going on above:

  1. First you deserialize the data using NSJSONSerialization.
  2. You need to check each and every subscript value in the JSON object to make sure that it is not nil. Once you’re sure it has a valid value, search for the next object. Once you’ve made your way through all of the subscripts, you’ll get the name and link values to work with.

    Note that if any element in the JSON is unexpected, you bail on the whole mess and the app name never gets printed. This is desirable in this case.

  3. The last step is initializing an App using the values of name and link and printing it to the console.

Save and run your storyboard; you should see the following result in your debugger console:

App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")

Yes — “Game of War – Fire Age” is the #1 app in the JSON file.

It took a lot of verbose code just to retrieve the name of the first app — it’s time to see how Gloss stacks up.

Introduction to JSON object mapping

Object mapping is a technique to turn JSON objects into native Swift objects. After defining model objects and corresponding mapping rules, Gloss does the hard work by performing the parsing instead of you.

It has significant benefits over the approach you just tried:

  • You code will be cleaner, reusable and easier to maintain.
  • You can work with objects rather than generic arrays and dictionaries.
  • You can extend model classes to add additional functionality.

Sounds good, huh? Let’s see how it works!

Parsing JSON the Gloss way

To keep everything nice and clean, create a new playground called Gloss.playground, then copy over topapps.json into Resources and DataManager.swift into Sources.

Integrating Gloss in Your Project

It’s fairly straightforward to integrate Gloss into your project/playground:

  1. Click the following link Gloss Repo Zip File and save the library to a convenient location.
  2. Unzip it and drag the Gloss-master/Gloss/Gloss folder into your Sources folder of your playground.

Your Project Navigator should look like this:
JSON tutorial

That’s it! Now that you have Gloss added to your playground, you can start parsing JSON without the optional binding headache!

Note: Gloss can be installed via Cocoapods as well. Since playgrounds are not supported yet, you can only use this method when working with regular Xcode projects.

Mapping JSON to objects

First, you have to define how your model objects relate to your JSON document.

Model objects must conform to the Decodeable protocol, which enables them to be decoded from JSON. To do so, you’ll implement the init?(json: JSON) initializer as defined in the protocol.

Have a look at the structure of topapps.json and create the data model!

TopApps

The TopApps model represents the top level object, and it contains a single key-value pair:

{
  "feed": {
    ...
  }
}

Create a new Swift file called TopApps.swift in the Sources folder of your playground and add the following code:

public struct TopApps: Decodable {
 
  // 1
  public let feed: Feed?
 
  // 2
  public init?(json: JSON) {
    feed = "feed" <~~ json
  }
 
}
  1. Define your model’s properties. In this case there is only one. You’ll define the Feed model object later.
  2. Make sure TopApps conforms to the Decodable protocol by implementing the custom initializer.
  3. You might wondering what <~~ is! It's called the Encode Operator and it is defined in Gloss's Operators.swift file. Basically it tells Gloss to grab the value which belongs to the key feed and encode it. Feed is a Decodable object as well; so Gloss will delegate the responsibility of the encoding to this class.

Feed

The Feed object is very similar to the top level object. Feeds have two key-value pairs, but since you are only interested in the top 25 apps, it's not necessary to process the author object.

{
  "author": {
    ...
  },
  "entry": [
    ...
  ]	
}

Create a new Swift file called Feed.swift in the Sources folder of your playground and define Feed as follows:

public struct Feed: Decodable {
 
  public let entries: [App]?
 
  public init?(json: JSON) {
    entries = "entry" <~~ json
  }
 
}

App

App is the last model object you are going to define. It represents an app in the chart:

{
  "im:name": {
    "label": "Game of War - Fire Age"
  },
  "id": {
    "label": "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2",	
    ...
  },
  ...
}

Create a new Swift file called App.swift in the Sources folder of your playground and add the following code:

public struct App: Decodable {
 
  // 1
  public let name: String
  public let link: String
 
  public init?(json: JSON) {
    // 2
    guard let container: JSON = "im:name" <~~ json,
      let id: JSON = "id" <~~ json
      else { return nil }
 
    guard let name: String = "label" <~~ container,
      link: String = "label" <~~ id
      else { return nil }
 
    self.name = name
    self.link = link
  }
 
}
  1. Feed and TopApp both used optional properties. It is possible to define a property as non-optional if you are sure that the JSON being used will always contain the values to populate them.
  2. You don't necessarily have to create model objects for every member in the JSON. For example, in this case it makes no sense to create a model for in:name and id. When working with non-optional and nested objects, always make sure to check for nil.

Now that your model classes are ready the only thing left is to let Gloss do it's job! :]

Open the playground file and add the following code:

import UIKit
import XCPlayground
 
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
 
DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in
  var json: [String: AnyObject]!
 
  // 1
  do {
    json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? [String: AnyObject]
  } catch {
    print(error)
    XCPlaygroundPage.currentPage.finishExecution()
  }
 
  // 2
  guard let topApps = TopApps(json: json) else {
    print("Error initializing object")
    XCPlaygroundPage.currentPage.finishExecution()
  }
 
  // 3
  guard let firstItem = topApps.feed?.entries?.first else {
    print("No such item")
    XCPlaygroundPage.currentPage.finishExecution()
  }
 
  // 4
  print(firstItem)
 
  XCPlaygroundPage.currentPage.finishExecution()
 
}
  1. First you deserialize the data using NSJSONSerialization. Same way you did before.
  2. Initialize an instance of TopApps by feeding the JSON data into it's constructor.
  3. Get the #1 app by simply getting the first entry of the feed.
  4. Print the app to the console.

Seriously — that's all the code you need.

Save and run your storyboard; you'll see that you successfully get the app name again, but this time in a more elegant way:

App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")

That takes care of parsing local data — but how would you parse data from a remote source?

Retrieving Remote JSON

It's time to make this project a bit more realistic. Normally, you'd be retrieving remote data, not data from a local file. You can easily grab the App Store ratings using an online request.

Open DataManager.swift and define the Top Apps URL:

let TopAppURL = "https://itunes.apple.com/us/rss/topgrossingipadapplications/limit=25/json"

then add the following method to DataManager's implementation:

public class func getTopAppsDataFromItunesWithSuccess(success: ((iTunesData: NSData!) -> Void)) {
  //1
  loadDataFromURL(NSURL(string: TopAppURL)!, completion:{(data, error) -> Void in
      //2
      if let data = data {
        //3
        success(iTunesData: data)
      }
  })
}

The above code looks pretty familiar; but instead of retrieving a local file you're using NSURLSession to pull the data from iTunes. Here's what's happening in detail:

  1. First you call loadDataFromURL; this takes the URL and a completion closure that passes in an NSData object.
  2. Next you make sure the data exists using optional binding.
  3. Finally, you pass the data to the success closure as you did before.

Open your storyboard and replace the line

DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in

with

DataManager.getTopAppsDataFromItunesWithSuccess { (data) -> Void in

Now you're retrieving real data from iTunes.

Save and run your storyboard; you'll see that the data parsing still arrives at the same conclusion of the #1 app:

App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")

The above value may be different for you, as the top apps in the App Store change constantly.

Usually people aren't just interested in the top app in the App Store — they want to see a list of all top apps. You don't have to code anything in order to access them - you can easily access them with the following code snippet:

topApps.feed?.entries

Peeking Under the Hood at Gloss

You can definitely see that Gloss works really well at taking care of the parsing process — but how does it work under the hood?

<~~ is a custom operator for a set of Decoder.decode functions. Gloss has built-in support for decoding a lot of types:

  • Simple types (Decoder.decode)
  • Decodable models (Decoder.decodeDecodable)
  • Simple arrays (Decoder.decode)
  • Arrays of Decodable models (Decoder.decodeDecodableArray)
  • Enum types (Decoder.decodeEnum)
  • Enum arrays (Decoder.decodeEnumArray)
  • NSURL types (Decoder.decodeURL)
  • NSURL arrays (Decode.decodeURLArray)
Note:If you want to learn more about Custom Operators take a look at the tutorial on the subject: Operator Overloading in Swift Tutorial

In this tutorial you relied heavily on Decodable models. If you need something more complex, it's possible to extend Decoder and provide your own decoding implementation.

Of course Gloss can convert objects back to JSON as well. If you are interested in doing that, have a look at the Encodable protocol.

Where to Go From Here?

Here is the final playground from the above JSON tutorial.

You can definitely use the playground as starter code for your next app; simply replace the URL for retrieving remote data with your own URL, handle the individual keys and indices of your new JSON response, and you'll have a new project that parses something else like the results of football matches or any other piece of data that can be downloaded over the network as JSON.

Development on Gloss will continue here on Github, so keep an eye out for the latest updates.

Swift is still evolving as well, so you'll want to keep up to date on Apple's documentation for any future updates for the language.

I hope you enjoyed this JSON tutorial; don't forget to check out all the other Swift tutorials on the website. If you have any questions or comments, feel free to join the discussion below!

The post Swift Tutorial: Working with JSON appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>