Skip to content

Instantly share code, notes, and snippets.

@JoshuaSullivan
Last active June 23, 2016 15:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshuaSullivan/63e58e4f7f07558c793567f31f73e0da to your computer and use it in GitHub Desktop.
Save JoshuaSullivan/63e58e4f7f07558c793567f31f73e0da to your computer and use it in GitHub Desktop.
This is a Swift Playground file implementing a solution to Challenge Accepted #49.
//: # 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