2

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."

Obi Anachebe
  • 123
  • 1
  • 13
  • 1
    You should never block main thread as it is responsible for drawing UI components onto screen and handling user interaction with them. You should make your UI fully functional and look normal instead when there is no data. – Ozgur Vatansever Sep 05 '16 at 02:28
  • @ozgur gotcha. So would that mean that once I retrieve that data, then I should refresh the view? I've tried reloading the data in my swipeView, but doesn't seem to solve the issue – Obi Anachebe Sep 05 '16 at 02:31
  • 1
    Yes, if your UI relies on displaying some content from a remote resource, the basic flow goes like this: you disable user interaction by showing some loading indicator, make the call to retrieve the content, then update your data source controlling the content and reload the UI. – Ozgur Vatansever Sep 05 '16 at 02:35
  • @ozgur ok so how would I stage a loading screen to wait for the data to be retrieved in this sequence 1. viewLoads and loading screen appears 2. Loading screen appears 3. Cache or server is called to retrieve data 4. View is set with values 5. Loading screen is removed Currently, the loading screen doesn't wait until the data has been retrieved before it is removed, because things are occurring asynchronously – Obi Anachebe Sep 05 '16 at 02:37
  • In similar questions, I read that I need to use a completion handler to allow the data to display. Is this the case for this problem and if so, could someone provide an example of how to do it for this? – Obi Anachebe Sep 05 '16 at 03:00

4 Answers4

1

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()
}
Obi Anachebe
  • 123
  • 1
  • 13
1

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()
    }

}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks @Rob I've updated my answer to reflect your suggestions. Now the issue I'm having is I keep getting an Alamofire error for "Invalid token given" when I log in and out of different accounts on the same device. I think it might be trying to use the accessToken before it's been set in `viewDidLoad()`. You think this is also an async issue? – Obi Anachebe Sep 05 '16 at 18:24
  • Ok talking myself through this as I see the debugger. What's happening is the cahce block doesn't wait for the accessToken to be set before running, so the instead of kicking the user off and going to the login screen, it loads the home screen with nil values before it gets a chance to fire the error and present the log in screen. So should I require that checking for a valid token happens before caching and everything else, and if so, would I use dispatch groups to make that happen? – Obi Anachebe Sep 05 '16 at 18:52
  • sorry man I'm unfamiliar with the platform! I'll edit and give you credit – Obi Anachebe Sep 05 '16 at 18:53
  • 1
    This token stuff sounds like a new question, but it sounds like a logic flow issue, not a dispatch group thing. But it's a bit hard to follow precisely on the basis of your description. But it sounds like you have a working theory! At the very least, though, I'd make sure to (a) remove the old token when the user logs off; and (b) add some error handling if you do a request when the token is `nil`. That way, you'll at least be able to correctly identify the problem, rather than sending requests with invalid tokens. – Rob Sep 05 '16 at 18:55
  • @ObiAnachebe - I'd suggest you continue your diagnostics and if you really hit a road block, post a new question with a [reproducible example of the problem](http://stackoverflow.com/help/mcve). But comments here are not really the right place to continue to diagnose this different issue. – Rob Sep 05 '16 at 18:59
  • 1
    sounds good man. I'll keep debugging and post a new question if it's an issue. Thanks a bunch for the help! – Obi Anachebe Sep 05 '16 at 19:03
0

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
            ...
    }
}
Willjay
  • 6,381
  • 4
  • 33
  • 58
  • I tried this with a progressHUD() function I made for the view and although it waits until after the asynchronous call is completed to remove the HUD, it still loads the view with nil values. Should I make the loading screen a separate view and present that while the cache is fetching data, then remove that loading view? – Obi Anachebe Sep 05 '16 at 04:23
0

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.

Zachary Espiritu
  • 937
  • 7
  • 23
  • This was really helpful. Thanks! So for my problem, I tried calling updateEntries() in the dispatch_group_async block and some other code in the dispatch_group_notify block, but code in the notify block still got executed first. Is this because I have asynchronous calls in updateEntries(). My guess is that I have to put my cache.fetch() call in the dispatch_group_async block. Yea or Nay? – Obi Anachebe Sep 05 '16 at 04:35
  • Yes! This fixed it! Thanks so much! – Obi Anachebe Sep 05 '16 at 04:45
  • Actually, this only fixed it when I call the API to retrieve data :/. When it pulls from the cache, it seems like that is still getting done after everything else – Obi Anachebe Sep 05 '16 at 04:50
  • @ObiAnachebe - Yes, this `dispatch_group_async` will not work if the code you're calling is, itself, asynchronous. What you do in that case is `dispatch_group_enter` before you call the asynchronous code, `dispatch_group_leave` in the completion handler of the asynchronous method, and then `dispatch_group_notify` will work as outlined by Zachary. – Rob Sep 05 '16 at 06:11
  • @Rob thanks a bunch for this. It seems to be working well accept when I retrieve data from the cache. See my update. Feel like I'm really close – Obi Anachebe Sep 05 '16 at 13:38