This is a Swift Playground file implementing a solution to Challenge Accepted #49.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//: # Challenge Accepted #49 | |
//: ## WUnderground API Client | |
//: ### Version 2.0 | |
//: | |
//: Implement the networking layer for the Umbrella weather app(iOS or Android), | |
//: using a similar architecture to the one demonstrated in the first episode of Swift Talks. | |
//: | |
//: - note: | |
//: This is my updated solution to the challenge which demonstrates a more powerful and flexible | |
//: approach to customizing the request sent to the API through the use of closures rather than | |
//: static URLs / endpoint paths. Also, the documentation is better. | |
import UIKit | |
import XCPlayground | |
// This is required for any playground with asynchronous callbacks. | |
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true | |
/// The WUnderground API Key | |
let apiKey = "7363d24e6b924e33" | |
/// The zip code to fetch weather for. | |
let zipcode = "55317" | |
//: - note: | |
//: When you're working with JSON, it is useful to include this `typealias` to make code | |
//: easier to parse. | |
/// JSON parses in to this format of Dictionary. | |
typealias JSONDictionary = [String: AnyObject] | |
/// A general-purpose function that accepts NSData and attempts to convert it into a JSONDictionary. | |
func parseDataToJSONDictionary(data: NSData) throws -> JSONDictionary { | |
guard let dict = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? JSONDictionary else { | |
throw APIRequestError.incorrectRootJSONType | |
} | |
return dict | |
} | |
//: Create our primary data object. | |
/// A simple data structure to parse our hourly weather data into. | |
struct Forecast { | |
/// The date/time that the forecast is for. | |
let date: NSDate | |
/// The predicted weather condition at the time of the forecast. | |
let condition: String | |
/// The temperature (in °F) predicted in the forecast. | |
let temperature: String | |
} | |
//: - note: | |
//: This is a failable initializer that takes a `JSONDictionary` instead of the default arguements. | |
//: We are declaring this in an extension so that it doesn't replace the default initializer. | |
extension Forecast { | |
init?(json: JSONDictionary) { | |
// Get the date/time. | |
guard | |
let timeJSON = json["FCTTIME"] as? JSONDictionary, | |
let epochString = timeJSON["epoch"] as? String, | |
let epoch = Double(epochString) | |
else { | |
debugPrint("Unable to extract epoch value from JSON.") | |
return nil | |
} | |
self.date = NSDate(timeIntervalSince1970: epoch) | |
// Get the temperature. | |
guard | |
let tempJSON = json["temp"] as? JSONDictionary, | |
let temp = tempJSON["english"] as? String | |
else { | |
debugPrint("Unable to get temperature from JSON.") | |
return nil | |
} | |
self.temperature = temp | |
// Get the conditions. | |
guard let condition = json["condition"] as? String else { | |
debugPrint("Unable to get condition from JSON.") | |
return nil | |
} | |
self.condition = condition | |
} | |
} | |
//: This extension gives Forecast a factory method for creating APIRequests for hourly data. This is | |
//: useful because the only thing that changes from request to request is the zip code. | |
extension Forecast { | |
/// Factory method to create an APIRequest for Hourly Forecast data. | |
/// | |
/// - parameter forZipcode: The US zipcode to get the forecast for. | |
/// - returns: A fully configured APIRequest. | |
static func createHourlyForecastRequest(forZipcode zip: String) -> APIRequest<[Forecast]> { | |
return APIRequest( | |
urlRequest: { baseURL -> NSURLRequest in | |
let path = "hourly/q/\(zip).json" | |
let request = NSURLRequest(URL: baseURL.URLByAppendingPathComponent(path)) | |
// If we wanted to add other configuration to the request here, we could. | |
return request | |
}, | |
parse: { optionalData -> [Forecast] in | |
do { | |
// Ensure we got the data we expected. | |
guard let data = optionalData else { throw APIRequestError.dataNotFound } | |
// Try to decode the data into JSON. | |
let json = try parseDataToJSONDictionary(data) | |
// Try to extract the forecast data from the JSON. | |
guard let hourlyForecasts = json["hourly_forecast"] as? [JSONDictionary] else { throw APIRequestError.parsingFailure } | |
// Convert the forecast data into an array of Forecast objects and sort them chronologically. | |
return hourlyForecasts.flatMap( Forecast.init ).sort(<) | |
} | |
}) | |
} | |
} | |
//: - note: | |
//: Conforming to `Equatable` and `Comperable` allows us to easily compare and sort our forecasts. | |
//: In a real-world application, the forecast would probably also have a location associated with it. | |
//: Generally speaking, you should always implement at least `Equatable` for your value types. | |
extension Forecast: Equatable {} | |
func ==(lhs: Forecast, rhs: Forecast) -> Bool { | |
return lhs.date == rhs.date | |
} | |
extension Forecast: Comparable {} | |
func <(lhs: Forecast, rhs: Forecast) -> Bool { | |
return lhs.date.timeIntervalSinceReferenceDate < rhs.date.timeIntervalSinceReferenceDate | |
} | |
//: ### Data Types | |
/// This generic type is useful for encapsulating the success or failure of an asynchronous action. | |
enum AsynchronousResult<T> { | |
/// The asynchronous action was successful and returned a result. | |
case success(result: T) | |
/// The asynchronous action failed. | |
case failure | |
} | |
/// Errors that may arise in the course of an API Request. | |
enum APIRequestError: ErrorType { | |
/// The JSON returned from the server had the wrong root type (array vs. dictionary). | |
case incorrectRootJSONType | |
/// The JSON data couldn't be parsed into the expected model objects. | |
case parsingFailure | |
/// The parser expected data in the API response and found none. | |
case dataNotFound | |
} | |
//: ### Request Object | |
/// A request for the API. It appears simple, but because both the `urlRequest` and | |
/// `parse` parameters are closures, they can implement arbitrarily complex business logic. | |
struct APIRequest<T> { | |
/// Accepts a base URL and returns a fully-configured URL request. | |
let urlRequest: (NSURL) throws -> NSURLRequest | |
/// Convert the response data (if there was any) into the DataType. | |
/// - note: In the case where no response data is expected, the generic type should be `Any?` and parse should return `nil`. | |
let parse: (NSData?) throws -> T | |
} | |
//: ### API Client | |
/// This class accepts objects conforming to APIRequestType to perform network interactions. | |
final class APIClient { | |
/// The base URL to make requests with. | |
let baseURL: NSURL | |
/// Having this passed in allows for easy configuration against different environments (dev, staging, production, etc.). | |
init(baseURL: NSURL) { | |
self.baseURL = baseURL | |
} | |
/// Loads data from the API based on the configuration of the request. | |
/// -Parameter request: The APIRequest to make to the API. | |
/// -Parameter completion: A completion block that accepts an AsynchronousResult with the same type as the APIRequest. | |
func load<T>(request: APIRequest<T>, completion:(AsynchronousResult<T>) -> Void) { | |
guard let urlRequest = try? request.urlRequest(baseURL) else { | |
preconditionFailure("Could not create URL Request.") | |
} | |
NSURLSession.sharedSession().dataTaskWithRequest(urlRequest) { | |
data, _, _ in | |
do { | |
let result = try request.parse(data) | |
completion(.success(result: result)) | |
} catch { | |
debugPrint("Request failed: \(error)") | |
completion(.failure) | |
} | |
}.resume() | |
} | |
} | |
//: ### Create and send a request for Hourly Forecast data. | |
/// An instance of the APIClient, pointed at the WUnderground API. | |
let apiClient = APIClient(baseURL: NSURL(string: "http://api.wunderground.com/api/\(apiKey)/")!) | |
/// Create the request with the factory method. | |
let hourlyForecastRequest = Forecast.createHourlyForecastRequest(forZipcode: zipcode) | |
// Load the request. | |
apiClient.load(hourlyForecastRequest) { | |
result in | |
switch result { | |
case .success(let forecasts): | |
/// This date formatter will make the date a little more pretty in the output. | |
let dateFormatter = NSDateFormatter() | |
dateFormatter.dateFormat = NSDateFormatter.dateFormatFromTemplate("Ehmm", options:0, locale: nil) | |
forecasts.forEach { | |
forecast in | |
// Nicely format some output for the console. | |
debugPrint("\(dateFormatter.stringFromDate(forecast.date)) – \(forecast.condition) (\(forecast.temperature)°)") | |
} | |
case .failure: | |
debugPrint("Failed to load and parse weather data. 😭") | |
} | |
// The playground has finished all it needs to do. Shut it down. | |
XCPlaygroundPage.currentPage.finishExecution() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment