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.
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.
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.