Building a Weather Application With Swift 3

Fetching Weather Data

Building a Weather Application With Swift 3
Resources

We built a basic user interface in the previous tutorial and it is now time to fetch weather data that we can show to the user. As I mentioned earlier, the application we are building uses the Dark Sky API to fetch data for a particular location.

This tutorial focuses on fetching weather data from Dark Sky and deserializing it, ready to use in the application. In this tutorial, I also show you how to organize the configuration we need to accomplish this.

If you want to follow along, clone or download the project from GitHub.

Namespacing Constants

Yesterday, I wrote about an undocumented side effect of nested types in Swift. You can use structures or enumerations to create namespaces for constants and convenience methods to access them. That is what we start with.

Why is that necessary? I have a fierce allergy for hardcoded values and string literals, especially if they are scattered throughout the codebase of a project. This is easy to solve with constants, but Swift adds a dash of elegance to this approach that I am particularly fond of.

In the Project Navigator, create a new group, Configuration, and a corresponding folder for the group. Add an empty file to the group and name it Configuration.swift.

Create a new group in the Project Navigator.

Create a new Swift file.

Add an import statement for the Foundation framework and create a structure, API.

import Foundation

struct API {

}

We use the API struct to namespace constants that are related to the Dark Sky API. We declare a BaseURL and an APIKey constant. Note that both constants are static properties. If you want to follow along, replace the value of APIKey with the API key of your Dark Sky developer account. You can create a free Dark Sky developer account on the Dark Sky website.

import Foundation

struct API {

    static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    static let BaseURL = URL(string: "https://api.darksky.net/forecast/")!

}

Note that we force unwrap the value that is assigned to BaseURL. I always emphasize to be careful with the use of forced unwrapped values. That said, since we are responsible for the value of BaseURL and we know that it is a valid URL at compile time, I consider it safe to force unwrap the value we assign to BaseURL. Moreover, the application we are creating would be of little use if BaseURL would not contain a valid value.

Before we move on, I would like to add a computed property to the API struct. This computed property returns the base URL for an authenticated request. It combines the value of BaseURL and APIKey.

import Foundation

struct API {

    static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    static let BaseURL = URL(string: "https://api.darksky.net/forecast/")!

    static var AuthenticatedBaseURL: URL {
        return BaseURL.appendingPathComponent(APIKey)
    }

}

I have added another struct to Configuration.swift, Defaults. This struct stores several default values that come in handy. For now, we use it to store the default coordinates of the location we fetch weather data for.

struct Defaults {

    static let Latitude: Double = 37.8267
    static let Longitude: Double = -122.423

}

Managing Weather Data

While it may be tempting to put a view controller in charge of fetching weather data from the Dark Sky API, it is better to hand this responsibility to a separate object. In the Project Navigator, Create a new group, Managers, and create a corresponding folder for the group. Create an empty file and name it DataManager.swift. Don't worry, though, we are not creating a singleton object.

Create a new group in the Project Navigator.

Create a new Swift file.

In DataManager.swift, we create a class, DataManager below the import statement of the Foundation framework. The class is marked as final to prevent subclassing and improve performance.

import Foundation

final class DataManager {

}

The data manager is in charge of handing weather data to its owner. This means that the owner, the root view controller, does not know where the weather data comes from. It could be stub data, cached data, or data fetched from the Dark Sky API. The consumer of the weather data, the root view controller, doesn't care about the implementation details.

To initialize an instance of the DataManager class, we require a base URL. This means that the DataManager class does not need to know which API it is communicating with. We declare a constant property for the base URL and an initializer that accepts a base URL.

import Foundation

final class DataManager {

    let baseURL: URL

    // MARK: - Initialization

    init(baseURL: URL) {
        self.baseURL = baseURL
    }

}

To request weather data, we implement a method that accepts three arguments:

  • a latitude
  • a longitude
  • a completion handler

The completion handler makes the implementation flexible. If the data manager decides to return cached data, the completion handler can be invoked immediately with the cached data. If no cached data is available or the cache is too old, a request is made to the Dark Sky API. The completion handler is invoked when the request completes.

import Foundation

enum DataManagerError: Error {

    case Unknown
    case FailedRequest
    case InvalidResponse

}

final class DataManager {

    typealias WeatherDataCompletion = (AnyObject?, DataManagerError?) -> ()

    let baseURL: URL

    // MARK: - Initialization

    init(baseURL: URL) {
        self.baseURL = baseURL
    }

    // MARK: - Requesting Data

    func weatherDataForLocation(latitude: Double, longitude: Double, completion: WeatherDataCompletion) {

    }

}

There are several details worth pointing out. At the top, we declare an enumeration for errors that may be returned by the DataManager class. As you probably know, an error type in Swift only needs to adopt the Error protocol. At the moment, we declare three cases:

  • Unknown
  • FailedRequest
  • InvalidResponse

We also declare a type alias for the completion handler of weatherDataForLocation(latitude:longitude:completion:). This is primarily for convenience. As I mentioned earlier, weatherDataForLocation(latitude:longitude:completion:) accepts three arguments, a latitude, a longitude, and a completion handler.

Requesting Weather Data

Caching is something we focus on later in this series. Whenever weatherDataForLocation(latitude:longitude:completion:) is invoked, we hit the Dark Sky API. This is very easy with the NSURLSession class as you can see below. Swift 3 makes the syntax even more elegant.

func weatherDataForLocation(latitude: Double, longitude: Double, completion: WeatherDataCompletion) {
    // Create URL
    let URL = baseURL.appendingPathComponent("\(latitude),\(longitude)")

    // Create Data Task
    URLSession.shared.dataTask(with: URL) { (data, response, error) in
        self.didFetchWeatherData(data: data, response: response, error: error, completion: completion)
        }.resume()
}

After creating the URL for the request, we ask the shared URL session object to create a data task with the URL. In the completion handler of the data task, we invoke a private helper method, didFetchWeatherData(data:response:error:completion:WeatherDataCompletion), in which we process the response of the network request.

// MARK: - Helper Methods

private func didFetchWeatherData(data: Data?, response: URLResponse?, error: Error?, completion: WeatherDataCompletion) {
    if let _ = error {
        completion(nil, .FailedRequest)

    } else if let data = data, let response = response as? HTTPURLResponse {
        if response.statusCode == 200 {
            processWeatherData(data: data, completion: completion)
        } else {
            completion(nil, .FailedRequest)
        }

    } else {
        completion(nil, .Unknown)
    }
}

In this helper method, we handle any errors or unexpected responses. If the status code of the network request is equal to 200, we process the response by invoking another private helper method, processWeatherData(data:completion:). The implementation is short and simple. We deserialize the Data object that is passed in using jsonObject(with:options:) of the JSONSerialization class.

private func processWeatherData(data: Data, completion: WeatherDataCompletion) {
    if let JSON = try? JSONSerialization.jsonObject(with: data, options: []) as AnyObject {
        completion(JSON, nil)
    } else {
        completion(nil, .InvalidResponse)
    }
}

Notice that we use the try? keyword instead of the try keyword. We are not interested in any errors that are thrown when deserializing the Data instance. We only need to know whether the response can be deserialized to a valid object. If we run into an issue during deserialization, we invoke the completion handler and pass in the appropriate error type.

If you want to learn more about the difference between try, try?, and try!, then I recommend another tutorial I recently wrote about error handling.

Using the Data Manager

Because the DataManager class encapsulates the details of managing weather data and communicating with the Dark Sky API, the implementation of the RootViewController class remains simple and uncluttered.

Open RootViewController.swift and create a private constant property of type DataManager.

private let dataManager = DataManager(baseURL: API.AuthenticatedBaseURL)

In the view controller's viewDidLoad() method, we ask the data manager for weather data for a particular location. We use the default values we created earlier.

override func viewDidLoad() {
    super.viewDidLoad()

    setupView()

    // Fetch Weather Data
    dataManager.weatherDataForLocation(latitude: Defaults.Latitude, longitude: Defaults.Longitude) { (response, error) in
        print(response)
    }
}

Run the application in the simulator and inspect the output in the console. You should see a lot of data logged to the console.

What's Next?

We now have some weather data to work with. But as you can see, the Dark Sky API returns a lot of weather data. We only need a fraction of that weather data and populate the user interface with it.

In the next tutorial, we pour the weather data into model objects to make handling the weather data easier. You can find the source files of this tutorial on GitHub.

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

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By