Building a Weather Application From Scratch

Fetching Weather Data

In the previous episode, we laid the foundation of Rainstorm's user interface. Before we continue building the user interface, I'd like to know what the weather data the application will use to populate its user interface looks like. In this episode, we fetch weather data from Dark Sky. The Dark Sky API is very easy to use. Create a Dark Sky developer account if you'd like to follow along. It's free and it only takes a minute.

URL Session

We won't be relying on a third party library for performing the request. The application sends a GET request to the Dark Sky API and the URLSession API is more than sufficient for this task. Let's start simple and improve the implementation as we go.

Open RootViewController.swift. We fetch the weather data in the viewDidLoad() method of the RootViewController class by invoking a helper method, fetchWeatherData().

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Setup Child View Controllers
    setupChildViewControllers()

    // Fetch Weather Data
    fetchWeatherData()
}

In this private method, we create the base URL of the request. Because the initializer of the URL struct returns an optional, we make use of a guard statement. Don't worry, though, we improve this later.

private func fetchWeatherData() {
    // Create Base URL
    guard let baseUrl = URL(string: "https://api.darksky.net/forecast/") else {
        return
    }
}

We need to append the API key to the base URL to authenticate the request. We invoke appendingPathComponent(_:) on baseUrl, passing in the API key. You can find the API key of your Dark Sky developer account on the Dark Sky website.

private func fetchWeatherData() {
    // Create Base URL
    guard let baseUrl = URL(string: "https://api.darksky.net/forecast/") else {
        return
    }

    // Append API Key
    let authenticatedBaseUrl = baseUrl.appendingPathComponent("f574c275ab31a264ea69d4ffea0fde52")
}

The coordinates of the location we request weather data for are the last component of the URL. The latitude and longitude of the location are separated by a comma. Swift's string interpolation makes this easy. We invoke appendingPathComponent(_:) on authenticatedBaseUrl, passing in the latitude and longitude of the location we're interested in.

private func fetchWeatherData() {
    // Create Base URL
    guard let baseUrl = URL(string: "https://api.darksky.net/forecast/") else {
        return
    }

    // Append API Key
    let authenticatedBaseUrl = baseUrl.appendingPathComponent("f574c275ab31a264ea69d4ffea0fde52")

    // Create URL
    let url = authenticatedBaseUrl.appendingPathComponent("\(37.335114),\(-122.008928)")
}

Performing the request isn't difficult thanks to the URLSession API. We access the shared URLSession instance through the shared class property and we ask it for a data task, an instance of the URLSessionDataTask class. We pass the dataTask(with:completionHandler:) method the URL instance and a closure. The closure is invoked when the request completes, successfully or unsuccessfully. It accepts three optional parameters, a Data instance, a URLResponse instance, and an Error instance.

private func fetchWeatherData() {
    // Create Base URL
    guard let baseUrl = URL(string: "https://api.darksky.net/forecast/") else {
        return
    }

    // Append API Key
    let authenticatedBaseUrl = baseUrl.appendingPathComponent("f574c275ab31a264ea69d4ffea0fde52")

    // Create URL
    let url = authenticatedBaseUrl.appendingPathComponent("\(37.335114),\(-122.008928)")

    // Create Data Task
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let error = error {
            print("Request Did Fail (\(error)")
        } else if let response = response {
            print(response)
        }
    }
}

Because the completion handler is the last argument of the dataTask(with:completionHandler:) method, we use trailing closure syntax. If the request fails, we print the error to the console. If the request succeeds, we print the URLResponse instance to the console.

Run the application and inspect the output in the console. If you're new to the URLSession API, you may be surprised to discover that the request isn't sent to the Dark Sky API. The URLSession instance has given us a data task, but we need to invoke resume() on the URLSessionDataTask instance to send the request to the Dark Sky API.

private func fetchWeatherData() {
    // Create Base URL
    guard let baseUrl = URL(string: "https://api.darksky.net/forecast/") else {
        return
    }

    // Append API Key
    let authenticatedBaseUrl = baseUrl.appendingPathComponent("f574c275ab31a264ea69d4ffea0fde52")

    // Create URL
    let url = authenticatedBaseUrl.appendingPathComponent("\(37.335114),\(-122.008928)")

    // Create Data Task
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        if let error = error {
            print("Request Did Fail (\(error)")
        } else if let response = response {
            print(response)
        }
    }.resume()
}

Let's give it another try. Run the application and inspect the output in the console. You should see the URLResponse instance printed to the console. The status code of the request is 200, indicating that the request completed successfully.

<NSHTTPURLResponse: 0x6180004215e0> { URL: https://api.darksky.net/forecast/f574c275ab31a264ea69d4ffea0fde52/37.335114,-122.008928 } { Status Code: 200, Headers {
    "Cache-Control" =     (
        "max-age=60"
    );
    "Content-Encoding" =     (
        gzip
    );
    "Content-Type" =     (
        "application/json; charset=utf-8"
    );
    Date =     (
        "Wed, 13 Jun 2018 05:45:59 GMT"
    );
    Expires =     (
        "Wed, 13 Jun 2018 05:46:58 +0000"
    );
    Vary =     (
        "Accept-Encoding"
    );
    "x-authentication-time" =     (
        1ms
    );
    "x-content-type-options" =     (
        nosniff
    );
    "x-forecast-api-calls" =     (
        1
    );
    "x-response-time" =     (
        "109.230ms"
    );
} }

That's a good start. This is the approach I almost always take when I implement a feature or fix a bug. I implement a basic version of what I'd like to achieve and go from there. Even though there are several aspects of the current implementation that need attention, we know that the application can successfully fetch weather data from the Dark Sky API.

Time For Some Improvements

Let's take a look at the current implementation and create a list of possible improvements we can make.

Object Literals

The base URL, the API key, and the coordinates are exposed to the root view controller as object literals. As I mentioned earlier in this series, I'm allergic to random string and number literals floating around in a codebase. The goal is to create a lightweight type that encapsulates the details of a request. That's the first improvement I plan to make.

Error Handling

The current implementation lacks error handling and we're currently not parsing the response of the Dark Sky API. The Dark Sky API returns a chunk of JSON. We need to parse the response and extract the bits and pieces the application needs to populate its user interface.

Model-View-ViewModel

The biggest issue concerns the architecture of the application. The root view controller is responsible for creating, sending, and handling the request. That's something I don't like and we resolve this by taking advantage of the Model-View-ViewModel pattern.

The root view controller should only be aware of the data it needs to populate its views if we adopt the MVVM pattern. It shouldn't know about the response and it shouldn't even know about the raw values of the weather data the application fetches from Dark Sky. The application should only hand the root view controller the values it needs to populate the views it manages. That's one of the benefits of MVVM. The testability of the project also improves dramatically if we adopt the MVVM pattern.

Removing Object Literals

We tackle each of these improvements in the next episodes. We start by removing the object literals from the RootViewController class.

Next Episode "Removing Object Literals"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By