Building a Weather Application With Swift 3

Decoding JSON Data in Swift: Part 1

Building a Weather Application With Swift 3
Resources

In the previous tutorial of this series, we fetched weather data from the Dark Sky API. Before we can feed the weather data to the user interface of the application, we need to pour it into model objects. Swift's strict type safety rules make this less trivial than it sounds.

In this and the next tutorial, we take a detour to take a closer look at three solutions to decode the weather data the application fetches from the Dark Sky API.

Xcode Playground

To find a suitable solution, we are going to use an Xcode playground. This makes it easier to test each solution in isolation before using it in the weather application we are creating.

Create a new playground in Xcode 8 and replace its contents with an import statement for the Foundation framework.

import Foundation

Reveal the Navigator on the right and open the Project Navigator. The playground contains a Sources group and a Resources group by default. Download a sample response from the Dark Sky API and add it to a file in the Resources group, response.json. We will be using this sample response to find a suitable solution.

Add Sample JSON Response

Load Sample Response Data

Before we can parse the JSON data, we need to load it from response.json and deserialize it. This is easy enough as you can see below.

import Foundation

// Fetch URL
let url = Bundle.main.url(forResource: "response", withExtension: "json")!

// Load Data
let data = try! Data(contentsOf: url)

// Deserialize JSON
let JSON = try! JSONSerialization.jsonObject(with: data, options: [])

We deserialize the Data instance using the JSONSerialization API. The API is easy to understand. We invoke jsonObject(with:options:), passing in a Data instance and a collection of options. Notice that I ignore error handling for now by using the try! keyword. Once we have a solution, we can focus on error handling in the weather application. If no error is thrown, jsonObject(with:options:) returns an object of type Any.

Create Model Objects

For this tutorial, I would like to extract two types of data from the JSON response:

  • the location the weather data is from
  • hourly weather data for that location

Take a moment to inspect the structure of the JSON response. The latitude and longitude of the location the weather data is from are located at the root of the JSON object. That is easy. The hourly weather data is nested within the JSON object. That is going to be a bit trickier.

JSON Data Structure

As you can see, the hourly weather data is an array of data points. This means that we need to create an array of model objects to store the hourly weather data for the location.

To store the weather data, we are going to create two structures, WeatherData and WeatherHourData. In the Project Navigator on the right, create two files in the Sources group:

  • WeatherData.swift
  • WeatherHourData.swift

This is what the implementation of the WeatherData struct looks like. Because any files in the Sources group are located in a module that is different from that of the playground, we need to set the access level of the structure and its properties to public. We also need to create a public initializer. Fortunately, this workaround won't be necessary in the Xcode project of the weather application.

import Foundation

public struct WeatherData {

    public let lat: Double
    public let long: Double

    public let hourData: [WeatherHourData]

    public init(lat: Double, long: Double, hourData: [WeatherHourData]) {
        self.lat = lat
        self.long = long
        self.hourData = hourData
    }

}

Note that hourData is of type [WeatherHourData]. This brings us to the implementation of the WeatherHourData struct. Add the following implementation to WeatherHourData.swift.

import Foundation

public struct WeatherHourData {

    public let time: Date
    public let windSpeed: Int
    public let temperature: Double
    public let precipitation: Double

    public init(time: Date, windSpeed: Int, temperature: Double, precipitation: Double) {
        self.time = time
        self.windSpeed = windSpeed
        self.temperature = temperature
        self.precipitation = precipitation
    }

}

Great. We have defined the structures we plan to pour the JSON data into. The next challenge is decoding the JSON data and creating instances of the structures using the weather data of the Dark Sky API.

Solution #1: Quick and Dirty

The first solution is going to be the easiest to understand. But this solution has several major drawbacks that become evident as we implement the solution.

We create an instance of WeatherData using the JSON response. This doesn't look too bad. Right?

if let JSON = JSON as? [String: AnyObject] {
    if let lat = JSON["latitude"] as? Double, let long = JSON["longitude"] as? Double {
        let weatherData = WeatherData(lat: lat, long: long, hourData: [])
    }
}

Even though it doesn't look terrible, take a look at the next snippet in which we parse the hourly weather data. This is starting to look like a bad idea.

if let JSON = JSON as? [String: AnyObject] {
    if let lat = JSON["latitude"] as? Double,
       let long = JSON["longitude"] as? Double,
       let hourlyData = JSON["hourly"]?["data"] as? [[String: AnyObject]] {

        // Create Buffer
        var hourData = [WeatherHourData]()

        for hourlyDataPoint in hourlyData {
            if let time = hourlyDataPoint["time"] as? Double,
               let windSpeed = hourlyDataPoint["windSpeed"] as? Int,
               let temperature = hourlyDataPoint["temperature"] as? Double,
               let precipitation = hourlyDataPoint["precipIntensity"] as? Double {
                // Convert Time to Date
                let timeAsDate = Date(timeIntervalSince1970: time)

                // Create Weather Hour Data
                let weatherHourData = WeatherHourData(time: timeAsDate, windSpeed: windSpeed, temperature: temperature, precipitation: precipitation)

                // Append to Buffer
                hourData.append(weatherHourData)
            }
        }

        let weatherData = WeatherData(lat: lat, long: long, hourData: hourData)
    }
}

We achieved the goal we set out to achieve, but the approach we took needs to change. The current implementation is too brittle and overly complex. Let me show you how we can improve this by using protocols and extensions.

Solution #2: Protocols and Extensions

We can improve the above solution with protocols and extensions. In the Project Navigator, create a new file in the Sources group, JSONDecodable.swift.

protocol JSONDecodable {

    init?(JSON: Any)

}

We declare a protocol, JSONDecodable, with one method, a failable initializer that accepts a parameter of type Any. If we make the WeatherData and WeatherHourData structures conform to this protocol, we can move most of the logic we wrote earlier to their corresponding structures.

Open WeatherData.swift and add an extension for the JSONDecodable protocol. In the failable initializer, we add the logic for creating an instance of the structure with an object of type Any. Note that the failable initializer is declared public to ensure we can use it in the playground.

extension WeatherData: JSONDecodable {

    public init?(JSON: Any) {
        guard let JSON = JSON as? [String: AnyObject] else { return nil }

        guard let lat = JSON["latitude"] as? Double else { return nil }
        guard let long = JSON["longitude"] as? Double else { return nil }
        guard let hourlyData = JSON["hourly"]?["data"] as? [[String: AnyObject]] else { return nil }

        self.lat = lat
        self.long = long

        var buffer = [WeatherHourData]()

        for hourlyDataPoint in hourlyData {
            if let weatherHourData = WeatherHourData(JSON: hourlyDataPoint) {
                buffer.append(weatherHourData)
            }
        }

        self.hourData = buffer
    }

}

We also need to conform the WeatherHourData structure to the JSONDecodable protocol. The implementation looks very similar. Note that the failable initializer is declared public to ensure we can use it in the playground.

extension WeatherHourData: JSONDecodable {

    public init?(JSON: Any) {
        guard let JSON = JSON as? [String: AnyObject] else { return nil }

        guard let time = JSON["time"] as? Double else { return nil }
        guard let windSpeed = JSON["windSpeed"] as? Int else { return nil }
        guard let temperature = JSON["temperature"] as? Double else { return nil }
        guard let precipitation = JSON["precipIntensity"] as? Double else { return nil }

        self.windSpeed = windSpeed
        self.temperature = temperature
        self.precipitation = precipitation
        self.time = Date(timeIntervalSince1970: time)
    }

}

Head back to the playground to see if everything is working as expected. To create an instance of the WeatherData structure, we invoke the init(JSON:) failable initializer. This returns an optional. In other words, if the JSON data isn't structured correctly, the initialization fails. We could also make the initializer throwing, which may be a better option.

if let weatherData = WeatherData(JSON: JSON) {
    print(weatherData)
}

Is That It?

Even though we have moved the logic for parsing JSON data to the model objects, this isn't the solution I would like to use in Thunderstorm. In the next tutorial, we refactor the playground to make use of generics to make the JSONDecodable protocol much more powerful and flexible.

You can download the source files of this tutorial from GitHub.

Resources
Next Episode "Decoding JSON Data in Swift: Part 2"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By