문제

Note: please don't comment on whether or not this is a good idea ;) I'm just experimenting so want to see how it pans out...

I've got a UITextView and some inputText, which I'm going to set as the text for the UITextView.

I want to try visually drip-feeding the inputText to the text view, say, one letter at a time or one word at a time.

For example (without any consideration for the drip-feed delay or thread of execution):

for (NSInteger i = 0; i < inputText.length; i++) {
    NSString* charAtIndex = [text substringWithRange:NSMakeRange(i, 1)];
    _textView.text = [NSString stringWithFormat:@"%@%@", _textView.text, charAtIndex];
}

So, to build in a delay and see the characters (or words) added, one at a time, I can do the following:

dispatch_queue_t backgroundQueue = dispatch_queue_create("background_queue", 0);
for (NSInteger i = 0; i < inputText.length; i++) {
    dispatch_async(backgroundQueue, ^{
        [NSThread sleepForTimeInterval:0.01f];
        NSString* charAtIndex = [inputText substringWithRange:NSMakeRange(i, 1)];
        dispatch_async(dispatch_get_main_queue(), ^{
            _textView.text = [NSString stringWithFormat:@"%@%@", _textView.text, charAtIndex];
        });    
    });
}

That works fine, and as expected, the for loop is queueing up a bunch of asynchronous operations on the background queue.

But I want the above to take place as part of a larger, single synchronous visualisation. Before adding this code to drip-feed the UITextView, I simply set the text in the UITextView and then animated the current view (including the UITextView) off-screen. All on the main thread. The user would tap a button, see the whole text appear and then the view starts to animate off the screen, right away. The user then moves on to the next step in the workflow.

I'm trying the drip-feed visualisation to give the impression of the text view being populated one character (or word) at a time, but I don’t want the user to have to wait until every last character/word has been added. So, I’d like to see the UITextView being populated for say, 0.3 or 0.5 of a second, before starting the animation that animates the view (including the UITextView) off-screen…

So it’s kinda like this:

dispatch_queue_t backgroundQueue = dispatch_queue_create("background_queue", 0);
for (NSInteger i = 0; i < inputText.length; i++) {
    dispatch_async(backgroundQueue, ^{
        [NSThread sleepForTimeInterval:0.01f];
        NSString* charAtIndex = [inputText substringWithRange:NSMakeRange(i, 1)];
        dispatch_async(dispatch_get_main_queue(), ^{
            _textView.text = [NSString stringWithFormat:@"%@%@", _textView.text, charAtIndex];
        });    
    });
}

// ...
// Animate the view (including the UITextView) off screen
// ...
// User continues with the main workflow

Right now, with all that drip-feeding happening asynchronously in the background, once I add in the code to animate the view out of the way, you miss the visual drip-feed. The main thread runs right through to animating the view off screen.

I’m not sure how to achieve what I want?

Do I interrupt the above loop in some way? Check for a flag that’s updated from another thread?

I can’t put the wait on the main thread - because it will prevent the drip-feed updates to the UITextView

Any suggestions?

도움이 되었습니까?

해결책 3

Based on what J. Costa suggested, I've stayed with the Grand Central Dispatch approach. The piece that I was missing (and I'm not sure that I explained this requirement very well) is the access to a shared resource.

In the following code, I've structured it so:

  • BOOL shared resource to indicate whether or not the drip-feed should continue
  • create a serial queue for reading from / writing to that shared resource
  • the drip-feed happens on a background queue and additionally uses the serial queue to check if it should continue
  • the delayed view animation is setup using dispatch_after, and when it occurs it uses the serial queue to signal the drip-feeding should stop

Code:

NSString* inputText = @"Some meaningful text...";
dispatch_queue_t serialQueue = dispatch_queue_create("serialqueue", DISPATCH_QUEUE_SERIAL);

// the shared resource
_continueDripFeed = YES;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    for (NSInteger i = 0; i < inputText.length; i++) {
        __block BOOL keepGoing = NO;
        dispatch_sync(serialQueue, ^{
            // read from the shared resource
            keepGoing = _continueDripFeed;
        });

        if (keepGoing) {
            [NSThread sleepForTimeInterval:0.02f];
            NSString* charAtIndex = [inputText substringWithRange:NSMakeRange(i, 1)];
            dispatch_async(dispatch_get_main_queue(), ^{
                _textView.text = [NSString stringWithFormat:@"%@%@", _textView.text, charAtIndex];
            });
        }
        else {
            dispatch_async(dispatch_get_main_queue(), ^{
                _textView.text = inputText;
            });
            break;
        }
    }
});

double delayInSeconds = 0.5f;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    dispatch_sync(serialQueue, ^{
        // update the shared resource
        _continueDripFeed = NO;
    });
    [self animateTextViewToFrame:_offScreenFrame];
    // Continue with workflow...
});

다른 팁

You can delay the animation to "give" time to the drip-feed, like this:

double delayInSeconds = 0.5f;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    // ...
    // Animate the view (including the UITextView) off screen
    // ...              
});

This way the user will see the drip-feed animation for 0.5s. This is the GCD version but you can use the method too:

[UIView animateWithDuration:0.3f // Set your duration here
                      delay:0.5f
                    options:UIViewAnimationOptionCurveEaseOut // Choose the right option here
                 animations:^{
                     // Do your animations here.
                 }
                 completion:^(BOOL finished){
                     if (finished) {
                         // Do your method here after your animation.
                     }
                 }];

You're better off not queuing up loads of tasks and then setting up another delayed animation. Yes, it will work, but it isn't so maintainable and doesn't cater well for other situations in the future.

Instead, think about using an NSTimer and a couple of instance variables (which could be wrapped up in another class to keep everything clean and tidy). The instance variable is basically the current progress (i from your loop). Each time the timer fires, check i - if the text animation isn't complete, use i to substring and update the UI. If the text animation is complete, invalidate the timer and start the final view animation.

In this way the logic is organised, easily understandable, reusable and cancellable.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top