iosswiftsvprogresshud

SVProgressHUD code executes out of order


I'm building a bus predictions app using the NextBus API that will help users get prediction times and bus information. I've implemented a function that takes the user's current location and a chosen address and returns a list of 10 bus routes that minimize travel distance and time.

Here's the @IBAction that triggers the aforementioned function:

@IBAction func findAWayPressed(_ sender: UIButton) {
    // Hide confirm button.
    confirmButton.isHidden = true

    // Setup loading HUD.
    let blue = UIColor(red: 153/255, green: 186/255, blue: 221/255, alpha: 1.0)
    SVProgressHUD.setBackgroundColor(blue)
    SVProgressHUD.setStatus("Finding a way for you...")
    SVProgressHUD.setBorderColor(UIColor.black)
    SVProgressHUD.show()

    // Finds a list of ten bus routes that minimizes the distance from the user and their destination.
    WayFinder.shared.findAWay(startCoordinate: origin!, endCoordinate: destination!)

    SVProgressHUD.dismiss()
}

The problem is that confirmButton.isHidden = true and the SVProgressHUD lines only seem to do anything after the WayFinder.shared.findAWay() executes. The HUD displays for a brief moment before being immediately dismissed by SVProgressHUD.dismiss().

Here's the findAWay function:

func findAWay(startCoordinate: CLLocationCoordinate2D, endCoordinate: CLLocationCoordinate2D) {
    // Get list of bus routes from NextBus API.
    getRoutes()

    guard !self.routes.isEmpty else {return}

    // Initialize the the lists of destination and origin stops.
    closestDestinations = DistanceData(shortestDistance: 1000000, stops: [])
    closestOrigins = DistanceData(shortestDistance: 1000000, stops: [])

    // Fetch route info for every route in NextBus API.
    var routeConfigsDownloaded: Int = 0
    for route in routes {
        // Counter is always one whether the request fails
        // or succeeds to prevent app crash.
        getRouteInfo(route: route) { (counter) in
            routeConfigsDownloaded += counter
        }
    }

    while routeConfigsDownloaded != routes.count {}

    // Iterate through every stop and retrieve a list
    // of 10 possible destination stops sorted by distance.
    getClosestDestinations(endCoordinate: endCoordinate)
    // Use destination stop routes to find stops near
    // user's current location that end at destination stops.
    getOriginStops(startCoordinate: startCoordinate)

    // Sort routes by adding their orign distance and destination
    // distance and sorting by total distance.
    getFoundWays()
}

private func getRouteInfo(route: Route, completion: @escaping (Int) -> Void) {
    APIWrapper.routeFetcher.fetchRouteInfo(routeTag: route.tag) { (config) in
        if let config = config {
            self.routeConfigs[route.tag] = config
        } else {
            print("Error retrieving route config for Route \(route.tag).")
        }
        completion(1)
    }
}

Why would the code in @IBAction not execute in order? How could the hud not show on screen before findAWay was called? Any ideas?


Solution

  • So, you're going to want to do some reading about the "main thread" and how it works. Maybe UNDERSTANDING THE IOS MAIN THREAD

    Basically, you're asking the system to show the HUD, then performing, what I assume is a long running and blocking operation, and then dismiss the HUD all within the main thread.

    It's impossible for the system to show the HUD until the method exists, as it will part of the next cycle (paint/layout/other important stuff). In cases like this, I would lean towards some kind of "promise" API, like PromiseKit or Hydra as it will greatly simply the thread hoping.

    The basic intent is - While on the main thread, present the HUD, using a background thread, execute the query, when it's complete, dismiss the HUD, but do so on the main thread.

    Which might look something like this..

    SVProgressHUD.show()
    DispatchQueue.global(qos: .userInitiated).async {
        WayFinder.shared.findAWay(startCoordinate: origin!, endCoordinate: destination!)
        DispatchQueue.main.async {
            SVProgressHUD.dismiss()
        }
    }
    

    Now remember, never modify the UI from outside the main thread context, if the OS detects this, it will crash your App!

    I might also consider using a DispatchSemaphore instead of a "wild running" while-loop, so instead of..

    // Fetch route info for every route in NextBus API.
    var routeConfigsDownloaded: Int = 0
    for route in routes {
        // Counter is always one whether the request fails
        // or succeeds to prevent app crash.
        getRouteInfo(route: route) { (counter) in
            routeConfigsDownloaded += counter
        }
    }
    
    while routeConfigsDownloaded != routes.count {}
    

    You might use something like...

    let semaphore = DispatchSemaphore(value: routes.count)
    // Fetch route info for every route in NextBus API.
    var routeConfigsDownloaded: Int = 0
    for route in routes {
        // Counter is always one whether the request fails
        // or succeeds to prevent app crash.
        getRouteInfo(route: route) { (counter) in
            semaphore.signal()
        }
    }
    
    semaphore.wait()
    

    which will do the same thing, but more efficiently