1

This is a fundamental problem with executing asynchronous blocks, and I haven't found a good solution yet.

The following code works, but it blocks the main thread, and I'd like to avoid that.

override func shouldPerformSegueWithIdentifier(identifier: String?, sender: AnyObject?) -> Bool {
    //check if transaction already exists
    let trans = PFQuery(className: "Transactions")
    trans.whereKey("itemId", equalTo: self.pfObject.objectId!)

   //I need the count of objects in the query before I can proceed, but I don't want to block the main thread
   let count = trans.countObjects()
   if(count > 0){
        return false
    }else{
        return true
    }
}

This problem isn't specific to this part of my application. Normally, I can just set "count" (or whatever variable I need) in the closure of something like query.countObjectsInBackGroundWithBlock(), but I can't do that when I need to return something on the main thread.

Is there a solution to make my application wait for return without blocking the main thread? I actually don't think that there is in this case without redesigning a large portion of code, but I just want to make sure I'm not being naive.

What are the accepted solutions for these types of problems?

LulzCow
  • 1,199
  • 2
  • 9
  • 21

2 Answers2

5

The proper solution for this is to not call anything from shouldPerformSegueWithIdentifier which won't finish very quickly (within milliseconds). If you need to perform some network request that may take a few seconds, you should:

  • Retire shouldPerformSegueWithIdentifier;

  • Remove the segue from your UI control to the next scene;

  • Instead, connect up your button to an @IBAction which will programmatically perform the asynchronous request, and only if you get the result you expected, manually perform the segue programmatically (see Switching Views Programmatically in Swift).

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Yes, but "you should not perform the segue" sounds a lot like answering NO to shouldPerformSegue. Better wording is "do not *initiate* a segue if you cannot synchronously do the work required by the segue. – danh Jul 21 '15 at 16:57
  • I've attempted to clarify. – Rob Jul 21 '15 at 17:02
  • agree. also worth pointing out that the asynch condition might be checked even earlier. e.g. If it's a button that triggers the change in UI, and that button won't work unless some condition is met, better style is to indicate that by disabling, hiding or otherwise decorating the button. Such a UI would require that the condition be checked while the data for the view loading. (Also note: parse.com discourages count queries). – danh Jul 21 '15 at 17:06
  • Thanks for the answer. I was slowly coming to this conclusion, and there is a straightforward solution to problems like the one I used as an example. It becomes a little more difficult to find a workaround when I'm working with delegate methods of certain frameworks which depend on something I'm querying for. @danh made a good point about disabling buttons or whatever - I've just been struggling to make the app look and feel good when I do something like that. – LulzCow Jul 21 '15 at 17:31
  • I disagree re disabling/hiding buttons. It's a poor UX for controls to appear/enable in a non-deterministic fashion (from user's perspective). I do enabling/disabling for stuff under the user's control (e.g. has the user has filled in all required fields), but not for stuff magically happening in the background. If I do some proactive background validation of the data, I don't tell the user about it (other than network activity indicator), but just have the app quietly do it, and if the user taps button, show spinner if it's not done, or immediately proceed if it is done. – Rob Jul 21 '15 at 17:41
0

Edit: this is basically Rob's answer with more characters

Rob's answer is very good. An alternative way, and please let me know if this is bad practice, is to call self.performSegueWithIdentifier (I hope I got the method name right, from the completion block of countObjectsInBackgroundWithBlock (or, as danh points out, Parse discourage count queries, use something else, maybe findObjectsInBackgroundWithBlock and count the [AnyObject]? array).

//IBAction method
func buttonFunc() { 
    let query = PFQuery(classname: "ClassName")
    query.whereKey(itemId, equalTo: self.pfObject.objectId!)
    query.findObjectsInBackgroundWithBlock {
    (objects: [AnyObject]?, error: NSError?) -> Void in 
        //maybe do some error checking etc.
        if objects.count > 1 {
        performSegueWithIdentifier(theParametersAndStuff)
        }
    }

}

If you need to pass the objects to the next ViewController you can prepareForSegue, since it will only be called if the conditional in the completion block of the buttonFunc returns true.

(as I said, if someone think this is bad practice, please tell me why, I'm curios :) )

Edit: Of course you could put a Activity Indicator or similar. An alternative way is to preload that data, before the current viewController is shown, to either enable or disable the button triggering the segue (as danh pointed out).

Mattias
  • 415
  • 3
  • 13
  • I don't think this is bad practice: This is precisely what I was suggesting he do. Lol. – Rob Jul 21 '15 at 17:43
  • Ah, I misread your answer, I'm sorry. I thought you meant `presentViewController()`. My bad! – Mattias Jul 21 '15 at 17:44
  • 1
    No worries. That linked answer presents both options and I agree w you that `performSegueWithIdentifier` is preferable. – Rob Jul 21 '15 at 17:45