Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Objective-C, cancel a dispatch queue using UI event

Scenario:

  • User taps a button asking for some kind of modification on address book.
  • A method is called to start this modification and an alert view is shown.
  • In order to show the alert view and keep the UI responsive, I used dispatch_queue:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                     dispatch_sync(dispatch_get_main_queue(), ^{
                       // Show the alert view
                     });
                   });
    
  • Start the process of address book modification using:

    dispatch_async(modifyingAddressBookQueue, ^{});
    

Now, I want to provide the user with the ability to cancel the process anytime (of course before saving the address book). So when he taps the cancel button in the alert sheet, I want to access the dispatch block, set some certain BOOL to stop the process and revert the address book.

The problem is, you can't do that! you can't access the block and change any variable inside it since all variables are copied only once. Any change of variables inside the block while being executed won't be seen by the block.

To sum up: How to stop a going operation using a UI event?

Update:

The code for the process:

- (void) startFixingModification {

    _fixContacts = YES;
    __block BOOL cancelled = NO;

    dispatch_queue_t modifyingAddressBookQueue;
    modifyingAddressBookQueue = dispatch_queue_create(sModifyingAddressBookQueueIdentifier,
                                                      NULL);

    dispatch_async(modifyingAddressBookQueue, ^{

        for (NSMutableDictionary *contactDictionary in _contactArray) {

            if (!cancelled) {
                break;
            }

            i = i + 1;

            BOOL didFixContact = [self fixNumberInContactDictionary:contactDictionary];
            if (!didFixContact) {
                _fixedNumbers = _fixedNumbers - 1;
            }

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                dispatch_sync(dispatch_get_main_queue(), ^{
                    [self setAlertViewProgress:i];
                });

            });
        }
    });

    cancelledPtr = &cancelled;

}

Code for alertview (my own lib) delegate

- (void) alertViewProgressCancel:(ASAlertViewProgress *)alertView { // This is a private lib.


    if (cancelledPtr)
    {
        NSLog(@"stopping");

        *cancelledPtr = YES;
    }

}

In interface, I declare

BOOL*   cancelledPtr;

Update 2:

It's getting really frustrating! for the following code

for (NSMutableDictionary *contactDictionary in _contactArray) {

            NSLog(@"%d", _cancelModification);
            if (_cancelModification) {
                break;
            }
}

if _cancelModification is set to YES, the for loop is broken and that's OK. Once I comment out the NSLog line, the _cancelModification is neglected when it changes to YES!

like image 314
Abdalrahman Shatou Avatar asked Apr 08 '12 21:04

Abdalrahman Shatou


Video Answer


2 Answers

If you declare your BOOL using __block, then it can be changed outside of the block execution, and the block will see the new value. See the documentation for more details.

An example:

@interface SNViewController ()
{
    BOOL*   cancelledPtr;
}

@end

@implementation SNViewController

- (IBAction)start:(id)sender
{
    __block BOOL cancelled = NO;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (!cancelled) {
            NSLog(@"running");
            sleep(1);
        }        
        NSLog(@"stopped");
    });

    cancelledPtr = &cancelled;
}

- (IBAction)stop:(id)sender
{
    if (cancelledPtr)
    {
        NSLog(@"stopping");

        *cancelledPtr = YES;
    }
}

@end

Alternatively, use an ivar in your class to store the BOOL. The block will implicitly make a copy of self and will access the ivar via that. No need for __block.

@interface SNViewController ()
{
    BOOL   cancelled;
}

@end

@implementation SNViewController

- (IBAction)start:(id)sender
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (!cancelled) {
            NSLog(@"running");
            sleep(1);
        }        
        NSLog(@"stopped");
    });
}

- (IBAction)stop:(id)sender
{
    NSLog(@"stopping");
    cancelled = YES;
}

@end
like image 188
Kurt Revis Avatar answered Oct 04 '22 01:10

Kurt Revis


Approach 1

Create a custom dispatch_async method that returns a "cancelable" block.

// The dispatch_cancel_block_t takes as parameter the "cancel" directive to suspend the block execution or not whenever the block to execute is dispatched. 
// The return value is a boolean indicating if the block has already been executed or not.
typedef BOOL (^dispatch_cancel_block_t)(BOOL cancelBlock);

dispatch_cancel_block_t dispatch_async_with_cancel_block(dispatch_queue_t queue, void (^block)())
{
    __block BOOL execute = YES;
    __block BOOL executed = NO;

    dispatch_cancel_block_t cancelBlock = ^BOOL (BOOL cancelled) {
        execute = !cancelled;
        return executed == NO;
    };

    dispatch_async(queue, ^{
        if (execute)
            block();
        executed = YES;
    });

    return cancelBlock;
}

- (void)testCancelableBlock
{
    dispatch_cancel_block_t cancelBlock = dispatch_async_with_cancel_block(dispatch_get_main_queue(), ^{
        NSLog(@"Block 1 executed");
    });

    // Canceling the block execution
    BOOL success1 = cancelBlock(YES);
    NSLog(@"Block is cancelled successfully: %@", success1?@"YES":@"NO");

    // Resuming the block execution
    // BOOL success2 = cancelBlock(NO);
    // NSLog(@"Block is resumed successfully: %@", success2?@"YES":@"NO");
}

Approach 2

Defining a macro for executing a block asynchronously if a condition is validated:

#define dispatch_async_if(queue,condition,block) \
dispatch_async(queue, ^{\
    if (condition == YES)\
        block();\
});

- (void)testConditionBlock
{
    // Creating condition variable
    __block BOOL condition = YES;

    dispatch_async_if(dispatch_get_main_queue(), condition, ^{
        NSLog(@"Block 2 executed");
    });

    // Canceling the block execution
    condition = NO;

    // Also, we could use a method to test the condition status
    dispatch_async_if(dispatch_get_main_queue(), ![self mustCancelBlockExecution], ^{
        NSLog(@"Block 3 executed");
    });
}
like image 28
vilanovi Avatar answered Oct 04 '22 02:10

vilanovi