Recusiwe Recusiwe - 5 months ago 11
Swift Question

Wait for data to get loaded in model before accessing them in ViewController

I'm struggling with letting my ViewVontroller know when all the data are loaded successfully. My ViewController has a list of Stop objects, all these objects have a list of Bus objects. In each Stop I call an API and populate the list of Bus objects. I got completion blocks for knowing when the job is done, in each Stop object. However, when I want to use them in my ViewController I'm am pretty much lost how to do it. I want to make sure that every single Stop object has a complete list of Busses, before I use them. The solution at the moment is to wait for 2 seconds and then use them, not a preferable solution, but it gives me some data to work with. Any suggestions? I guess I can't call a method from my ViewController in my model, since this would break MVC-pattern, right?

class LoadingViewController: UIViewController {

var nearbyStops = [Stop]()

override func viewDidLoad() {
super.viewDidLoad()
animateLogoEntry()

GPSUtil.sharedInstance.startGPS()

stopsNearby(initialLocation, maxRadius: 1000, maxNumber: 5)

}

// MARK: - API work

/**
Assigns the array of Stops to the nearby stops
*/
func stopsNearby(location: CLLocationCoordinate2D, maxRadius: Int, maxNumber: Int) {
APIHelper.sharedInstance.getStopsNearby(location, maxRadius: maxRadius, maxNumber: maxNumber) { (result) -> Void in
self.nearbyStops = result!
self.test()
}
}

func test() {
let time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), 2 * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue()) {
}
for item in self.nearbyStops {
let items = item.getDepartures()
print(item.name!)
for element in items {
element.timeUtilDeparture()
print(" " + element.name + " - " + element.direction)
print(" \(element.times)")
print(element.timeUntilDepartureList)
}
}
}
}

class Stop: NSObject, MKAnnotation {

var departures = [Departure]()
var busses = [Bus]()

init(name: String, coordinate: CLLocationCoordinate2D, stopID: Int64, distance: Int) {
self.name = name
self.coordinate = coordinate
self.stopID = stopID
self.distance = distance
super.init()
loadDepartures() { (result) -> Void in
self.sanitizeBusDepartures( { (result) -> Void in
// THE CURRENT OBJECT IS LOADED WITH DATA
})
}
}

/**
Loads the departures for the current stop.
*/
private func loadDepartures(completion: ((result: Bool) -> Void)!) {
let date = NSDate()
let calender = NSCalendar.currentCalendar()
let components = calender.components([.Hour, .Minute, .Day, .Month, .Year], fromDate: date)
let currentHour = components.hour
let currentMinute = components.minute
let currentDay = components.day
let currentMonth = components.month
let currentYear = components.year

let currentDate = "\(currentDay).\(currentMonth).\(currentYear)"
let currentTime = "\(currentHour):\(currentMinute)"

let specificURL = "URL"

let url: NSURL = NSURL(string: specificURL)!
let session = NSURLSession.sharedSession()

let task = session.dataTaskWithURL(url)
{ (data, resonse, error) -> Void in
if error == nil {

let dataReturned = JSON(data: data!)

let departures = dataReturned["DepartureBoard"]["Departure"]
for (_, value):(String, JSON) in departures {
let busName = value["name"].stringValue
let stop = value["stop"].stringValue
let time = value["time"].stringValue
let delay = value["messages"].intValue
let date = value["date"].stringValue
let finalStop = value["finalStop"].stringValue
let direction = value["direction"].stringValue
let journeyDetailUrl = value["JourneyDetailRef"]["ref"].stringValue

self.departures.append(
Departure(name: busName,
stop: stop,
time: time,
delay: delay,
date: date,
finalStop: finalStop,
direction: direction,
journeyDetailLink: journeyDetailUrl))
}
}

completion(result: true)
}

task.resume()
}

private func sanitizeBusDepartures(completion: ((result: Bool) -> Void)!) {
var tempList = [Bus]()

for item in departures {
tempList.append(Bus(name: item.name!, direction: item.direction!, times: item.time!))
}

for item in tempList {
if busses.contains(item) {
let indexOfElement = busses.indexOf(item)
busses[indexOfElement!].times += item.times
} else {
busses.append(item)
}

}

completion(result: true)
}


As you can see in the above every Bus object loads it's own data from it's own URL, so in my ViewController I want to be sure that every Stop object is loaded with data.

Answer

Is a bad idea to make assumptions about time when you work with async requests, yes, dispatch_groups would work, but I propose a simpler solution, delegates, create a protocol StopDelegate or some name that makes sense to you, every Stop object can have a stopDelegate variable:

protocol StopDelegate: class {
   func stopDidFetchDepartures(stop: Stop)
}

class Stop: NSObject, MKAnnotation {
   weak var stopDelegate: StopDelegate?
   ...
}

The Stop object informs it's delegate when the departures and busses information is ready:

loadDepartures() { (result) -> Void in
    self.sanitizeBusDepartures( { (result) -> Void in
        // THE CURRENT OBJECT IS LOADED WITH DATA
        self.stopDelegate. stopDidFetchDepartures(self)
    })
}

When you assign the nearbyStops variable of the LoadingViewController you can assign the controller as the delegate of every stop object and implement the delegate, you also need a counter of the stops that already finished loading:

var stopDeparturesFetchedCount = 0

func stopDidFetchDepartures(stop: Stop) {
   stopDeparturesFetchedCount += 1
   if stopDeparturesFetchedCount >= nearbyStops.count {
      print("All stops finished loading")
   }
}
Comments