Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

iOS Network Programming with Swift 2 Implementing a Forecast Client Testing the Networking Stack

Paul M
Paul M
16,370 Points

I get error 20 or MissingHTTPResponseError when running my Stormy app, can anyone tell me why?

I have no console errors, and no errors or warnings within the code. So why do I get an error?

Here is my APIClient.swift

import Foundation

public let TRENetworkingErrorDomain = "com.treehouse.Stormy.Networking"
public let MissingHTTPResponseError: Int = 10
public let UnexpectedResponseError: Int = 20

typealias JSON = [String : AnyObject]
typealias JSONTaskCompletion = (JSON?, NSHTTPURLResponse?, NSError?) -> Void
typealias JSONTask = NSURLSessionDataTask

enum APIResult<T> {
    case Success(T)
    case Failure(ErrorType)
}

protocol JSONDecodable {
    init?(JSON: [String : AnyObject])
}

protocol Endpoint {
    var baseURL: NSURL { get }
    var path: String { get }
    var request: NSURLRequest { get }
}

protocol APIClient {
    var configuration: NSURLSessionConfiguration { get }
    var session: NSURLSession { get }

    func JSONTaskWithRequest(request: NSURLRequest, completion: JSONTaskCompletion) -> JSONTask
    func fetch<T: JSONDecodable>(request: NSURLRequest, parse: JSON -> T?, completion: APIResult<T> -> Void)
}

extension APIClient {
    func JSONTaskWithRequest(request: NSURLRequest, completion: JSONTaskCompletion) -> JSONTask {

        let task = session.dataTaskWithRequest(request) { data, response, error in

            guard let HTTPResponse = response as? NSHTTPURLResponse else {
               let userInfo = [
                NSLocalizedDescriptionKey: NSLocalizedString("Missing HTTP Response", comment: "")
                ]

                let error = NSError(domain: TRENetworkingErrorDomain, code: MissingHTTPResponseError, userInfo: userInfo)
                completion(nil, nil, error)
                return
            }

            if data == nil {
                if let error = error {
                    completion(nil, HTTPResponse, error)
                }
            } else {
                switch HTTPResponse.statusCode {
                case 200:
                    do {
                        let json = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [String : AnyObject]
                        completion(json, HTTPResponse, nil)
                    } catch let error as NSError {
                        completion(nil, HTTPResponse, error)
                    }
                default: print("Recieved HTTP Response: \(HTTPResponse.statusCode)- not handled")
                }
            }
        }

        return task
    }

    func fetch<T>(request: NSURLRequest, parse: JSON -> T?, completion: APIResult<T> -> Void) {
        let task = JSONTaskWithRequest(request) { json, response, error in
            guard let json = json else {
                if let error = error {
                    completion(.Failure(error))
                } else {
                    // TODO: Implement Error Handling
                }
                return
            }

            if let value = parse(json) {
                completion(.Success(value))
            } else {
                let error = NSError(domain: TRENetworkingErrorDomain, code: UnexpectedResponseError, userInfo: nil)
                completion(.Failure(error))
            }
        }

        task.resume()
    }
}

ForecastClient.swift

import Foundation

struct Coordinate {
    let lattitude: Double
    let longintude: Double
}

enum Forecast: Endpoint {
    case Current(token: String, coordinate: Coordinate)

    var baseURL: NSURL {
        return NSURL(string: "https://api.forecast.io")!
    }

    var path: String {
        switch self {
        case .Current(let token, let coordinate):
            return "/forecast/\(token)/\(coordinate.lattitude),\(coordinate.longintude)"
        }
    }

    var request: NSURLRequest {
        let url = NSURL(string: path, relativeToURL: baseURL)!
        return NSURLRequest(URL: url)
    }

    }


final class ForecastAPIClient: APIClient {

    let configuration: NSURLSessionConfiguration
    lazy var session: NSURLSession = {
        return NSURLSession(configuration: self.configuration)
    }()

    private let token: String

    init(config: NSURLSessionConfiguration, APIKey: String) {
        self.configuration = config
        self.token = APIKey
    }

    convenience init(APIKey: String) {
        self.init(config: NSURLSessionConfiguration.defaultSessionConfiguration(),
                  APIKey: APIKey)
    }

    func fetchCurrentWeather(coordinate: Coordinate, completion: APIResult<CurrentWeather> -> Void) {
        let request = Forecast.Current(token: self.token, coordinate: coordinate).request

        fetch(request, parse: { json -> CurrentWeather? in
            // Parse from JSON to CurrentWeather

            if let currentWeatherDictionary = json["currently"] as? [String: AnyObject] {
                return CurrentWeather(JSON: currentWeatherDictionary)
            } else {
                return nil
            }

            }, completion: completion)
    }
}

And my ViewController.swift

import UIKit

extension CurrentWeather {
    var temperatureString: String {
        return "\(Int(temperature))º"
    }

    var humidityString: String {
        let percentageValue = Int(humidity * 100)
        return "\(percentageValue)%"
    }

    var percipitationProbabilityString: String {
        let percentageValue = Int(percipitationProbability * 100)
        return "\(percentageValue)%"
    }
}

class ViewController: UIViewController {

    @IBOutlet weak var currentTemperatureLabel: UILabel!
    @IBOutlet weak var currentHumidityLabel: UILabel!
    @IBOutlet weak var currentPrecipitationLabel: UILabel!
    @IBOutlet weak var currentWeatherIcon: UIImageView!
    @IBOutlet weak var currentSummaryLabel: UILabel!
    @IBOutlet weak var refreshButton: UIButton!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    lazy var forecastAPIClient = ForecastAPIClient(APIKey: "#########") // The API Key is correct, I just hashed it out.
    let coordinate = Coordinate(lattitude: 37.8267, longintude: -122.423)


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        forecastAPIClient.fetchCurrentWeather(coordinate) {
            result in
            switch result {
            case .Success(let currentWeather):
                dispatch_async(dispatch_get_main_queue()) {
                    self.display(currentWeather)
                }
            case .Failure(let error as NSError):
                dispatch_async(dispatch_get_main_queue()) {
                    self.showAlert("Unable to retrieve forecast", message: error.localizedDescription)
                }

            default: break
            }
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func display(weather: CurrentWeather) {
        currentTemperatureLabel.text = weather.temperatureString
        currentPrecipitationLabel.text = weather.percipitationProbabilityString
        currentHumidityLabel.text = weather.humidityString
        currentSummaryLabel.text = weather.summary
        currentWeatherIcon.image = weather.icon
    }

    func showAlert(title: String, message: String?, style: UIAlertControllerStyle = .Alert) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: style)
        let dismissAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
        alertController.addAction(dismissAction)

        presentViewController(alertController, animated: true, completion: nil)
    }


}
Brian Uribe
Brian Uribe
3,488 Points

I'm getting the same error

1 Answer

Jacky Chung
Jacky Chung
10,913 Points

Hey there,

I had the same problem and tested each part of the code. I knew the api request part works because I could print out the JSON result.

The problem was in the parse part. In JSON returned from the forecast.io, it looks like this

{
"time":1470382297,
" summary":"Overcast",
"icon":"cloudy",
"nearestStormDistance":15,
"nearestStormBearing":341,
"precipIntensity":0,
"precipProbability":0,
"temperature":55.76,
"apparentTemperature":55.76,
"dewPoint":53.03,
"humidity":0.91,
"windSpeed":6.69,
"windBearing":253,
"visibility":6.45,
"cloudCover":0.98,
"pressure":1012.65,
"ozone":304.24
}

I used precipitationProbability to find the property in my CurrentWeather Decodable. The name in the JSON returned is called precipProbability.

I don't know if you wrote the same thing as mine. Hope it helps!