Snarf
Snarf

Reputation: 15

Establish Error Case in NSURLConnection, JSON Parsing in Swift

I am attempting to pull information from a JSON on my website. In doing so, if there is an error with the connection, it should return that error and log it to the console. The problem I am running into is that if I turn on Airplane more or otherwise lose signal, the error: fatal error: unexpectedly found nil while unwrapping an Optional value crashes the application. Why is it returning this, when I have set conditions for the error to simply be logged? Thank you in advance!

I am unsure why the error is not being logged and preventing the application from crashing. Any pointers would be helpful.

JSONLoader.swift

import Foundation

var arrayOfMeals: [Meal] = [Meal]()
var weekDayArray = ["monday"]


func getJSON(urlToRequest: String) -> NSDictionary {

    var url: NSURL = NSURL(string: urlToRequest)!
    var jsonRequest: NSURLRequest = NSURLRequest(URL: url)
    var jsonResponse: AutoreleasingUnsafeMutablePointer<NSURLResponse?> = nil

    var error: NSError?
    var dataValue: NSData =  NSURLConnection.sendSynchronousRequest(jsonRequest, returningResponse: jsonResponse, error:&error)!

    if error? == nil {
        var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(dataValue, options: NSJSONReadingOptions.MutableContainers , error: &error) as NSDictionary
        NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData
        if error? == nil {
            return jsonResult
        }
        else {
            return NSDictionary(object: "Error: Something with parsing went wrong :(", forKey: "error")
        }
    }
    else {
        return NSDictionary(object: "Error: There was an error with your connection :(", forKey: "error")
    }

}

func loadJSON(jsonDictionary: NSDictionary) {

    for days in weekDayArray{
        var resultsArray = jsonDictionary[days] as NSArray
        for obj: AnyObject in resultsArray{
            let breakfast = (obj.objectForKey("breakfast")! as String)
            let lunch = (obj.objectForKey("lunch")! as String)
            let dinner = obj.objectForKey("dinner")! as String
            let dateString = obj.objectForKey("dateString")! as String
            let dayOfWeek = obj.objectForKey("dayOfWeek")! as String
            let newMeal = Meal(breakfast: breakfast, lunch: lunch, dinner: dinner, dayOfWeek: dayOfWeek, dateString: dateString)
            if theDays(newMeal.dateString) >= 0 {
                arrayOfMeals.append(newMeal)
            }
        }
    }


}

I expect the function getJSON to establish an NSURLConnection to the site, parse the JSON date, and return an NSDictionary. An error case is established if there is an error in the connection, then it returns the dictionary with a value explaining the error. An additional error case occurs to parse the JSON file, and returns an NSDictionary with similar error.

I expect the function loadJSON to create instances of an object Meal, which I define as having properties of breakfast, lunch, dinner, a date, and a day of the week. The values of this instance are the results of the returned NSDictionary from function getJSON. If the day is the future, append it to my arrayOfMeals. Otherwise, ignore the instance.

MealViewController

override func viewDidLoad() {
    super.viewDidLoad()

    var req = getJSON("http://www.seandeaton.com/meals/Mealx")
    loadJSON(req)


}

MealModel.swift - to create instances of the meals

class Meal {

    let breakfast: String
    let lunch: String
    let dinner: String
    let dayOfWeek: String
    let dateString: String

    init(breakfast: String, lunch: String, dinner: String, dayOfWeek: String, dateString: String) {

        self.breakfast = breakfast
        self.lunch = lunch
        self.dinner = dinner
        self.dayOfWeek = dayOfWeek
        self.dateString = dateString

    }

}

How can I prevent the app from crashing upon failure to establish a connection and log the error message to the console? Thanks again.

Upvotes: 1

Views: 1487

Answers (1)

Rob
Rob

Reputation: 437632

The key observation is that in the call to sendSynchronousRequest in which you're retrieving the NSData, you have defined NSData to not be optional, and you appended a ! which will force the unwrapping of the optional return value. Thus if the optional NSData was nil (which would happen if there were any network problems), this code would fail.

Instead, you should simply leave the NSData as an optional (i.e. a NSData?) and check whether it was nil or not before trying to unwrap it. Thus, I might suggest something like:

func retrieveJSON(urlToRequest: String) -> NSDictionary {

    let url: NSURL = NSURL(string: urlToRequest)!
    let jsonRequest: NSURLRequest = NSURLRequest(URL: url)

    var jsonResponse: NSURLResponse?
    var error: NSError?
    let dataValue = NSURLConnection.sendSynchronousRequest(jsonRequest, returningResponse: &jsonResponse, error:&error)

    if dataValue != nil {
        if let jsonResult = NSJSONSerialization.JSONObjectWithData(dataValue!, options: .MutableContainers, error: &error) as? NSDictionary {
            return jsonResult
        } else {
            return [
                "error": "Error: Something with parsing went wrong :(",
                "localizedDescription": error?.localizedDescription ?? ""
            ];
        }
    }
    else {
        return [
            "error": "Error: There was an error with your connection :(",
            "localizedDescription": error?.localizedDescription ?? ""
        ];
    }
}

The above will fix crashes resulting from the forced unwrapping of the optional NSData, which would have been be nil if there were networking problems. Note, you don't show us the code that calls getJSON and then subsequently calls loadJSON, but I assume you're doing the necessary checking for error handling there.

Note, I also retired the awkward AutoreleasingUnsafeMutablePointer<NSURLResponse?> construct. I also added the localizedDescription of the error to the dictionary being returned.

Personally, I'd generally return the complete NSError object and the NSURLResponse objects, so that the caller can diagnose the precise nature of the error, rather than just text descriptions.


In a more radical edit to your code, I'd suggest you generally avoid synchronous requests. Never perform synchronous requests from the main thread.

For example, you might define the method to perform the request asynchronously, like so:

func retrieveJSON(urlToRequest: String, completionHandler:(responseObject: NSDictionary?, error: NSError?) -> ()) {

    let url: NSURL = NSURL(string: urlToRequest)!
    let jsonRequest: NSURLRequest = NSURLRequest(URL: url)

    var jsonResponse: NSURLResponse?
    NSURLConnection.sendAsynchronousRequest(jsonRequest, queue: NSOperationQueue.mainQueue()) {
        response, data, error in

        if data == nil {
            completionHandler(responseObject: nil, error: error)
        } else {
            var parseError: NSError?
            let jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: &parseError) as NSDictionary?
            completionHandler(responseObject: jsonResult, error: error)
        }
    }
}

You'd then call it, using the "trailing closure syntax", like so:

retrieveJSON(urlString) {
    responseObject, error in

    if responseObject == nil {
        // handle error here, e.g.

        println(error)
        return
    }

    // handle successful the `NSDictionary` object, `responseObject`, here
}

// but don't try to use the object here, because the above runs asynchronously

Upvotes: 1

Related Questions