Swift: Retrieve value from asynchronous call before view appears

1.8k views Asked by At

I'm using HanekeSwift to retrieve cached data and then set it to labels in a swipeView every time the view appears. My code retrieves the data no problem, but because cache.fetch() is asynchronous, when I call my method to update the view, my labels are set to nil. Is there anyway to tell swift to wait until my cached data is retrieved before loading the view?

See code below:

override func viewWillAppear(animated: Bool) {
    updateEntries() // updates entries from cache when view appears
}

func updateEntries() {
    guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
    guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
    cache.fetch(key: cachedEntryKey).onSuccess { data in
        ...
        // if successful, set labels in swipeView to data retrieved from cache
        ...
        dispatch_group_leave(dispatchGroup)
    } .onFailure { error in
        print(error)
        ...
        // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
        ...
        dispatch_group_leave(dispatchGroup)
    }
}

When I step through the above code, it always displays the view and then steps into the cache block. How do I make viewWillAppear() allow updateEntries() to complete and not return out of it until the cache block is executed? Thanks a ton in advance!

Update 1:

The solution below is working pretty well and my calls are made in the correct sequence (my print statement in the notify block executes after the cache retrieval), but my views only update their labels with non-nil values when the server is called. Maybe I'm lumping the wrong code in the notify group?

override func viewWillAppear(animated: Bool) {
    self.addProgressHUD()
    updateEntries() // updates entries from cache when view appears
}

func updateEntries() {
    guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
    guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }

    let dispatchGroup = dispatch_group_create()
    dispatch_group_enter(dispatchGroup)

    dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
        cache.fetch(key: cachedEntryKey).onSuccess { data in
            ...
            // if successful, set labels in swipeView to data retrieved from cache
            ...
        } .onFailure { error in
            print(error)
            ...
            // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
            ...
        }
    }

    dispatch_group_notify(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
        print("Retrieved Data")
        self.removeProgressHUD()
    }

}

Update 2:

Also, I'm getting this warning in the console when I switch views. I think I'm locking up the main thread with the above code

"This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes. This will cause an exception in a future release."

4

There are 4 answers

6
Rob On BEST ANSWER

Note:

  • enter group before calling asynchronous method
  • leave group is each of the respective completion/failure handlers
  • dispatch UI updates in notify block to main queue

Thus:

func updateEntries() {
    guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
    guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }

    let group = dispatch_group_create()
    dispatch_group_enter(group)

    cache.fetch(key: cachedEntryKey).onSuccess { data in
        ...
        // if successful, set labels in swipeView to data retrieved from cache
        ...
        dispatch_group_leave(group)
    } .onFailure { error in
        print(error)
        ...
        // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
        ...
        dispatch_group_leave(group)
    }

    dispatch_group_notify(group, dispatch_get_main_queue()) {
        print("Retrieved Data")
        self.removeProgressHUD()
    }

}
1
Willjay On

Here's simple example that you can stage a loading screen. I just create a alert view, also you can create your custom loading indicator view instead.

let alert = UIAlertController(title: "", message: "please wait ...", preferredStyle: .alert)

override func viewWillAppear(animated: Bool) {
    self.present(alert, animated: true, completion: nil)
    updateEntries() // updates entries from cache when view appears
}

func updateEntries() {
    guard let accessToken = UserDefaults.standard.value(forKey: "accessToken") as? String,
          let cachedEntryKey = (accessToken + "food_entries.get") as? String else { 
      return 
    }
    cache.fetch(key: cachedEntryKey).onSuccess { data in
        ...
             // update value in your UI
             alert.dismiss(animated: true, completion: nil)
        ...
        } .onFailure { error in
            print(error)
            ...
                // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
            ...
    }
}
5
Zachary Espiritu On

While I entirely agree with @ozgur about displaying some sort of loading indicator from a UX standpoint, I figured the benefit of learning how to use Grand Central Dispatch (Apple's native solution to asynchronous waiting) might help you in the long-term.


You can use dispatch_groups to wait for a block(s) of code to completely finish running before running a completion handler of some sort.

From Apple's documentation:

A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.

[...]

The dispatch group keeps track of how many blocks are outstanding, and GCD retains the group until all its associated blocks complete execution.

Here's an example of dispatch_groups in action:

let dispatchGroup = dispatch_group_create()

dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
    // Run whatever code you need to in here. It will only move to the final
    // dispatch_group_notify block once it reaches the end of the block.
}

dispatch_group_notify(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
    // Code in here only runs once all dispatch_group_async blocks associated
    // with the dispatchGroup have finished completely.
}

The great part about dispatch_groups are that they allow you to run multiple asynchronous blocks at the same time and wait for all of them to finish before running the final completion handler. In other words, you can associate as many dispatch_group_async blocks with the dispatchGroup as you want.

If you wanted to go for the loading indicator approach (which you should), you can run code to display the loading indicator, then move into a dispatch_group with a completion handler to remove the loading indicator and load data into view once the dispatch_group completes.

0
Obi Anachebe On

Ok suggestions from everyone helped a ton on this. Think I got it. I need to make sure my cache block isn't blocking the main queue. See code below

EDIT

Thanks to @Rob for helping me make the proper adjustments to make this work

let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)

cache.fetch(key: cachedEntryKey).onSuccess { data in
    ...
    // if successful, set labels in swipeView to data retrieved from cache
    ...
    dispatch_group_leave(dispatchGroup)
} .onFailure { error in
    print(error)
    ...
    // if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
    ...
    dispatch_group_leave(dispatchGroup)
}

dispatch_group_notify(dispatchGroup, dispatch_get_main_queue()) {
    print("Retrieved Data")
    self.removeProgressHUD()
}