1

I'm currently building a weather application where I have one container ViewController managing multiple child ViewControllers, whose main views live inside the container ViewController's UIScrollView.

Each child VC has a location property, which is the class that holds all the weather information. This class has the following function for loading the data from the OpenWeatherMaps API:

func refresh(completionHandler: () -> ()) {
    let session = NSURLSession.sharedSession()
    session.dataTaskWithURL(self._apiUrl) { (responseData: NSData?, response:NSURLResponse?, error: NSError?) -> Void in
        if let data = responseData {
            do {
                let json: AnyObject = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)
                // parse json
                completionHandler()
            } catch let err as NSError {
                print(err.debugDescription)
            }
        }
    }.resume()
}

Then the child VC has a function calling the other function with the completionHandler running on the main thread (because I'm updating the user interface):

func refreshWeatherData() {
    location.refresh {
        dispatch_async(dispatch_get_main_queue(), {
            print("\(self.location.name)")
            self.forecastCollectionView.reloadData()
        })
    }
}

And finally from the ContainerVC I'm calling the refreshWeatherData function on all my LocationVCs.

override func viewDidLoad() {
    super.viewDidLoad()
    let zurich = WALocation(name: "Zürich", id: "7287650", country: "CH", longitude: 8.53071)
    let shanghai = WALocation(name: "Shanghai", id: "1796236", country: "CN", longitude: 121.45)
    let boston = WALocation(name: "Boston", id: "4183849", country: "US", longitude: -83.78)
    let vancouver = WALocation(name: "Vancouver", id: "6173331", country: "CA", longitude: -123.11)

    addLocationControllerForLocation(shanghai)
    locationControllers[0].refreshWeatherData()
    addLocationControllerForLocation(boston)
    locationControllers[1].refreshWeatherData()
    addLocationControllerForLocation(zurich)
    locationControllers[2].refreshWeatherData()
    addLocationControllerForLocation(vancouver)
    locationControllers[3].refreshWeatherData()
}

Now the issue I encounter is that sometimes (not always), one two or three times, the JSONSerialisation throws an error (in the refresh function of the location): Error Domain=NSCocoaErrorDomain Code=3840 "No value." UserInfo ``{NSDebugDescription=No value.} When instead in my ContainerVC I do this:

override func viewDidLoad() {
    super.viewDidLoad()
    let zurich = WALocation(name: "Zürich", id: "7287650", country: "CH", longitude: 8.53071)
    let shanghai = WALocation(name: "Shanghai", id: "1796236", country: "CN", longitude: 121.45)
    let boston = WALocation(name: "Boston", id: "4183849", country: "US", longitude: -83.78)
    let vancouver = WALocation(name: "Vancouver", id: "6173331", country: "CA", longitude: -123.11)

    addLocationControllerForLocation(shanghai)
    locationControllers[0].refreshWeatherData()
    sleep(1)
    addLocationControllerForLocation(boston)
    locationControllers[1].refreshWeatherData()
    sleep(1)
    addLocationControllerForLocation(zurich)
    locationControllers[2].refreshWeatherData()
    sleep(1)
    addLocationControllerForLocation(vancouver)
    locationControllers[3].refreshWeatherData()
}

NSJSONSerialisation never fails. This makes me think it only fails when multiple requests are running asynchronously.

Any help would be greatly appreciated.

Nico
  • 194
  • 1
  • 12

1 Answers1

0

This sounds like a threading issue. Have you tried enclosing the refresh calls (or refresh itself) in a GCD call? e.g.:

func refresh(completionHandler: () -> ()) {
    dispatch_async(dispatch_get_main_queue()){
        let session = NSURLSession.sharedSession()
        session.dataTaskWithURL(self._apiUrl) { (responseData: NSData?, response:NSURLResponse?, error: NSError?) -> Void in
            if let data = responseData {
                do {
                    let json: AnyObject = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)
                    // parse json
                    completionHandler()
                } catch let err as NSError {
                    print(err.debugDescription)
                }
            }
            }.resume()
    }
}

If need be, you could use dispatch_sync.

Either way, make sure that the completionHandler calls back to whatever it should on the right thread. If it's a UI update, then it's the main thread.

edit As a continuation from my post below, here's the update for the refresh to return data in the completionHandler.

func refresh(completionHandler: (AnyObject) -> ()) {  //Anyobject being your JsonData or whatever so could be NSString, String, etc...
    let session = NSURLSession.sharedSession()
    session.dataTaskWithURL(self._apiUrl) { (responseData: NSData?, response:NSURLResponse?, error: NSError?) -> Void in
        if let data = responseData {
            do {
                let json: AnyObject = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)
                // parse json
                completionHandler(json)
            } catch let err as NSError {
                print(err.debugDescription)
            }
        }
    }.resume()
}
Adrian Sluyters
  • 2,186
  • 1
  • 16
  • 21
  • I had already tried this, and it doesn't help. The only way I got it to work is by refreshing the next location in the completionHandler block of the location before... – Nico Apr 06 '16 at 16:56
  • Looks like something to do with the `self.location` call. Rather than that, would it not be better to pass back values via the completion handler? I'll give an example in my answer-post. – Adrian Sluyters Apr 06 '16 at 17:07
  • using that ensures that whatever is returned is whatever is sent to the completion handler... So there's no need to worry about threading really – Adrian Sluyters Apr 06 '16 at 17:12
  • I tested your code but it's not helping. I want to keep the json in my weather object anyway (because I'm updating the properties of that object while parsing). Interrestingly the json serialisation never fails when calling another API (used Star Wars API to test). It may be that I cannot call the API too often because I have a free acount (at openweathermaps). But thanks for your effort! – Nico Apr 06 '16 at 20:57
  • Now that you mention it, probably the REST side of the API only allows x requests per minuet to avoid DDoS attacks etc.... – Adrian Sluyters Apr 06 '16 at 21:14