1

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?

rickster
  • 124,678
  • 26
  • 272
  • 326
Jake
  • 25,479
  • 31
  • 107
  • 168
  • 1
    You don't *return* from an asynchronous call. Example here: http://stackoverflow.com/a/35358750/2227743 Also, you should not *force cast* the response, it may not be here, so check the error parameter first. – Eric Aya Mar 08 '16 at 20:20

1 Answers1

5

There are a few issues:

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

  2. 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
    
  3. 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 ...
        }
    }
    
  4. 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>
    
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • FWIW, I've updated the above to use contemporary Swift syntax, but I wouldn't generally use `JSONSerialization` and `Any` as types. We'd generally use `JSONDecoder` nowadays, but it seems like it's beyond the scope of this question... – Rob Feb 07 '21 at 01:52