Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I repeatedly loop dispatch_after statements?

I want to have a for loop with dispatch_after statements in them. Problem is that the dispatch_after calls don't seem to go in line with the for loop. In other words, I want it to only start the next iteration of the for loop after the statements in the dispatch_after block have executed.

How would I do this?

Use Case

I want to present words on screen. Traditionally I show one word per second. But depending on the word length, I now want to show longer words for slightly longer, and shorter words for slightly less time. I want to present a word, wait a little while (depending on how long the word is) then present the next word, wait a little while, then the next, etc.

like image 977
Doug Smith Avatar asked Sep 30 '13 14:09

Doug Smith


2 Answers

Prints 0,1,2,3,4,5,6,7,8,9, one digit per second.

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0UL);
    for (int i=0; i<10; i++) {
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        dispatch_async(queue,^{
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1LL * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
                NSLog(@"%d",i);
                dispatch_sync(dispatch_get_main_queue(), ^{
                    // show label on screen
                });
                dispatch_semaphore_signal(semaphore);
            });
        });
    }

If you state your use case, maybe there are other ways to accomplish what you are trying to do.


You could also accumulate the delay time in advance and send all blocks.

    (1) __block double delay = 0;
    (2) dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0UL);
    (3) for (int i=0; i<10; i++) {
    (4) delay += 1LL * NSEC_PER_SEC; // replace 1 second with something related to the length of your word
    (5) dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), queue, ^{
            NSLog(@"%d",i);
    (6)     dispatch_sync(dispatch_get_main_queue(), ^{
                // show label on screen
            });
        });
    }
  1. Mark the variable __block so it can be modified inside the block
  2. Get hold of one of the predefined queues of the system. A queue holds a number of anonymous pieces of code (called “blocks”) that are executed in turn.
  3. Loop.
  4. Add one second. You should replace that 1 with something else. For example, .10*[word length] will result in 1 second pause if the word has 10 letters.
  5. Put the following code (aka “block”) in the “queue” after the given amount of “delay” seconds. The queue will automatically run the blocks of code that you put on it. This line is like saying, “run this after a delay”.
  6. Anytime you are in the background (like here, because “queue” is probably running on a background thread) and you want to change the user interface, you have to run the code in the main queue, because UIKit is not a thread-safe library (meaning, not designed to run correctly if you call it from different threads).
like image 147
Jano Avatar answered Sep 28 '22 16:09

Jano


Here's one way to achieve this. Naturally, you will need to replace my NSLog with the code to show the word, and replace my simple 0.05 * word.length function with whatever function you're using to determine delay, but this should do the trick, and do it without blocking the presenting thread.

- (void)presentWord: (NSString*)word
{
    // Create a private, serial queue. This will preserve the ordering of the words
    static dispatch_queue_t wordQueue = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        wordQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
    });

    dispatch_async(wordQueue, ^{
        // Suspend the queue
        dispatch_suspend(wordQueue);

        // Show the word...
        NSLog(@"Now showing word: %@", word);

        // Calculate the delay until the next word should be shown...
        const NSTimeInterval timeToShow = 0.05 * word.length; // Or whatever your delay function is...

        // Have dispatch_after call us after that amount of time to resume the wordQueue.
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeToShow * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            dispatch_resume(wordQueue);
        });
    });
}

// There's nothing special here. Just split up a longer string into words, and pass them
// to presentWord: one at a time.
- (void)presentSentence: (NSString*)string
{
    NSArray* components = [string componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
    [components enumerateObjectsUsingBlock:^(NSString* obj, NSUInteger idx, BOOL *stop) {
        [self presentWord: obj];
    }];
}

EDIT: The way this works is that I'm using a serial queue to maintain the ordering of the words. When you submit a word to -presentWords it enqueues a block at the "back" of wordQueue. When that block begins execution, you know that wordQueue is not suspended (because you're in a block that's executing on wordQueue) and the first thing we do is suspend wordQueue. Since this block is already "in flight" it will run to completion, but no other blocks will be run from wordQueue until someone resumes it. After suspending the queue, we display the word. It will remain displayed until something else is displayed. Then, we calculate the delay based on the length of the word we just started showing, and set up a dispatch_after to resume wordQueue after that time has passed. When the serial queue is resumed, the block for the next word begins executing, suspends the queue and the whole process repeats itself.

like image 21
ipmcc Avatar answered Sep 28 '22 15:09

ipmcc