4

I want an easy way to drop code in the beginning of a method that will force the method to only be run on the main thread (because the method updates UI elements).

Currently, I have something like:

 if (![NSThread isMainThread]){
    [self performSelectorOnMainThread:_cmd withObject:_results waitUntilDone:NO];
    return;
}

but I want a way to include this in a macro without me having to enter the arguments for the method. It seems like there should be some way to iterate over the list of parameters passed to the current method and create an NSInvocation or similar. Any thoughts?

MechEthan
  • 5,703
  • 1
  • 35
  • 30
Adam Shiemke
  • 3,734
  • 2
  • 22
  • 23

5 Answers5

5

Will this work?

#define dispatch_main($block) (dispatch_get_current_queue() == dispatch_get_main_queue() ? $block() : dispatch_sync(dispatch_get_main_queue(), $block))

This will also work if you call it from the main thread too, a bonus. If you need asynchronous calling, just use dispatch_async instead of dispatch_sync.

Richard J. Ross III
  • 55,009
  • 24
  • 135
  • 201
  • 1
    dispatch_get_current_queue() is deprecated, use dispatch_main($block) ([NSThread mainThread] ? $block() : dispatch_async(dispatch_get_main_queue(), $block)) – Alaa Eldin Apr 09 '17 at 08:32
  • @AlaaEldin Thanks! +1 I've edited your suggestion into the original answer. – Albert Renshaw Jul 26 '18 at 06:44
  • @AlbertRenshaw Please do not modify old answers in cases like this. Better to make another answer with more updated information, especially in cases where you actually change the behavior of the code snippet here (main queue != main thread in all cases). – Richard J. Ross III Jul 30 '18 at 20:25
  • 1
    @RichardJ.RossIII My apologies; I've posted my edit as a new answer to avoid any further confusion – Albert Renshaw Jul 31 '18 at 00:11
  • @RichardJ.RossIII, On that logic, this answer should then be deleted or downvoted, as it is not working anymore. – Iulian Onofrei Nov 25 '19 at 14:03
3

Rather than trying to re-call your method on a different thread, I'd suggest using dispatch_sync() and dispatch_get_main_queue() to ensure that just the sensitive code is on the main thread. This can easily be wrapped in a function, as in Brad Larson's answer to "GCD to perform task in main thread".

His procedure is essentially the same as what you already have, the difference being that the code is put into a block and either called or enqueued as appropriate:

if ([NSThread isMainThread])
{
    blockContainingUICode();
}
else
{
    dispatch_sync(dispatch_get_main_queue(), blockContainingUICode);
}

Which could also be translated into a macro without a lot of trouble, if you so desire.

Creating the block itself doesn't require much of a change. If your UI code looks like this:

[[self textLabel] setText:name];

[[self detailTextLabel] setText:formattedDollarValue];

[[self imageView] setImage:thumbnail];

Putting it in a block to be enqueued just looks like this:

dispatch_block_t blockContainingUICode = ^{

    [[self textLabel] setText:mainText];

    [[self detailTextLabel] setText:detailText];

    [[self imageView] setImage:thumbnail];
};
Community
  • 1
  • 1
jscs
  • 63,694
  • 13
  • 151
  • 195
  • Warning! if you are on the main thread, this will block. See my question on why I think this is illogical, here: http://stackoverflow.com/questions/10984732/gcd-why-cant-we-use-a-dispatch-sync-on-the-current-queue – Richard J. Ross III Jun 13 '12 at 00:20
  • 1
    @RichardJ.RossIII Doesn't the above `if ([NSThread isMainThread]) { ... }` check avoid the deadlock? – MechEthan Jun 13 '12 at 00:22
  • @Richard: Of course it will block; that's what `dispatch_sync()` is supposed to do. Do you mean "deadlock"? It won't, due to the `isMainThread` test. You could also compare `dispatch_get_current_queue()` to `dispatch_get_main_queue()`. – jscs Jun 13 '12 at 00:22
  • I think the whole point was for him to not have to manually alter each function... to use dispatch_sync he would have to create a block containing the code in each function. – Michael Frederick Jun 13 '12 at 01:40
  • @Michael: Yes, you're absolutely right; I'm suggesting a slightly different approach because, as you also rightly pointed out, there's no general way to pass arguments along to a macro or other function. – jscs Jun 13 '12 at 01:42
2

This will work if called from main thread or background thread using NSThread conditional ternary.


Synchronous:

#define dispatch_main(__block) ([NSThread isMainThread] ? __block() : dispatch_sync(dispatch_get_main_queue(), __block))

Asynchronous:

#define dispatch_main(__block) ([NSThread isMainThread] ? __block() : dispatch_async(dispatch_get_main_queue(), __block))

To use:

-(void)method {
    dispatch_main(^{
        //contents of method here.
    });
}

Attribution: Inspired by Richard Ross's Answer

Albert Renshaw
  • 17,282
  • 18
  • 107
  • 195
1

The only way I know to create a dynamic NSInvocation from a method like that would require the arguments of your method to be a va_list.

You would need to be able to get the params of the current method as an array (so that you could loop the array and add the params to the NSInvocation) and I am not sure that this is possible (I don't think it is).

Michael Frederick
  • 16,664
  • 3
  • 43
  • 58
0

I think this is what you are looking for:

- (void)someMethod
{
    // Make sure the code will run on main thread

    if (! [NSThread isMainThread])
    {
        [self performSelectorOnMainThread:_cmd withObject:nil waitUntilDone:YES];

        return;
    }

    // Do some work on the main thread
}
arturgrigor
  • 9,361
  • 4
  • 31
  • 31