I am trying to get a JSON file from a URL and return the contents using Swift. However, the code fails at the line let httpResponse = response as! NSHTTPURLResponse
in the following code. I get an exception at this line and Xcode goes into debug mode.
class func downloadJSONFile()->AnyObject
{
let requestURL: NSURL = NSURL(string: "http://www.learnswiftonline.com/Samples/subway.json")!
let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
let session = NSURLSession.sharedSession()
var json:AnyObject = ""
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
do{
json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)
}catch {
print("Error with Json: \(error)")
}
}
}
task.resume()
return json
}
How can I fix this?
There are a few issues:
If there is any error in the request, response
will be nil
, and thus your attempt to force cast it will result in fatal error. Do not use forced unwrapping/casting when dealing with network responses.
There is a deeper problem here that you're trying to return data from a method that runs asynchronously. You should change your method to not return anything, per se, but rather supply a completion handler by which you can asynchronously pass back the relevant data:
class func downloadJSONFile(completionHandler: @escaping (Any?) -> Void) {
let requestURL = URL(string: "http://www.learnswiftonline.com/Samples/subway.json")!
let urlRequest = URLRequest(url: requestURL)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { data, response, error in
// check for fundamental networking errors
guard
error == nil,
let data = data
else {
print(error ?? "Other error")
completionHandler(nil)
return
}
guard
let httpResponse = response as? HTTPURLResponse,
(200 ..< 300) ~= httpResponse.statusCode
else {
print("Invalid status code")
completionHandler(nil)
return
}
do {
let json = try JSONSerialization.jsonObject(with: data)
completionHandler(json)
} catch let parseError {
print("Error parsing: \(parseError)")
completionHandler(nil)
}
}
task.resume()
}
and then you call it, using the completion handler (or use trailing closure syntax, like shown below):
APIClass.downloadJSONFile() { json in
guard json != nil else {
print("there was some problem")
return
}
// now you can use `json` here
dispatch_async(dispatch_get_main_queue()) {
// and if you're doing any model or UI updates, dispatch that back to the main queue
}
}
// but do not use `json` here, as the above runs asynchronously
Note, if you wanted to supply the error information back to the calling routine, you could change it to also return the error information, e.g.:
enum DownloadError: Error {
case networkError(Error)
case notHTTPResponse
case invalidHTTPResponse(Int)
case parseError(Error)
}
class func downloadJSONFile(completionHandler: @escaping (Result<Any, Error>) -> Void) {
let requestURL = URL(string: "http://www.learnswiftonline.com/Samples/subway.json")!
let urlRequest = URLRequest(url: requestURL)
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { data, response, error in
if let error = error {
completionHandler(.failure(DownloadError.networkError(error)))
return
}
guard let httpResponse = response as? HTTPURLResponse, let data = data else {
completionHandler(.failure(DownloadError.notHTTPResponse))
return
}
guard 200 ..< 300 ~= httpResponse.statusCode else {
completionHandler(.failure(DownloadError.invalidHTTPResponse(httpResponse.statusCode)))
return
}
do {
let json = try JSONSerialization.jsonObject(with: data)
completionHandler(.success(json))
} catch let parseError {
completionHandler(.failure(DownloadError.parseError(parseError)))
}
}
task.resume()
}
And, obviously, the call would change to take both parameters:
APIClass.downloadJSONFile() { result in
switch result {
case .failure(let error):
print(error)
case .success(let value):
// and then it would be like before ...
}
}
When using URLSession
in iOS 9 and later, it will not permit cleartext requests (i.e. "http" is not permitted, only "https" is, by default). You can force the app to permit non-https requests by adding the following to your info.plist
. See https://stackoverflow.com/a/31254874/1271826 for more information
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>learnswiftonline.com</key>
<dict>
<!--Include to allow subdomains-->
<key>NSIncludesSubdomains</key>
<true/>
<!--Include to allow HTTP requests-->
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<!--Include to specify minimum TLS version-->
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.1</string>
</dict>
</dict>
</dict>