2
votes

I have been trying to play around with this for a long time and I can't seem to find the best approach. I am getting confused because there seem to be different answers/opinions on how to accomplish this seemingly simple task.

I want to be able to have a reusable class called ActivityIndicatorController. This controller has two main methods: activateIndicator and deactivateIndicator. It takes a UIView as an argument/property as well as an NSString for a label. Upon activation, it will turn off user interaction in the UIView and add a rectangle subview (with alpha and rounded corners), a UIActivityIndicator control and a UILabel for the status text. This is desirable because that way I don't have to have custom UIActivityIndicatorView code in each view controller or to have to set up an ActivityIndicator in each NIB.

The problem I am fundamentally having is how to kick off this process of adding and animating the ActivityIndicator. Some methods I have tried don't display the new view at all. Others work, but the ActivityIndicator doesn't animate.

I have tried using [NSThread detachNewThreadSelector:@selector(startAnimating) toTarget:activityIndicator withObject:nil] inside the activateIndicator method, but that doesn't display the new UIView.

I have tried using [NSThread detachNewThreadSelector:@selector(activateIndicator) toTarget:activityIndicatorController withObject:nil] from a calling method, but this would put the whole creation of the new UIView in a separate thread.

Now to the question:

Part 1: I understand that all UI should be handled on the main thread, is that correct?

Part 2: What is the difference/advantage/disadvantage of using [NSThread detachThreadSelector] versus NSOperation?

Part 3: Is it better to:

(a) send the lengthy operation to a new background thread with a callback to the main thread OR

(b) send the UIActivityIndicatorView startAnimating method to a separate thread and run the lengthy process on the main thread

AND why?

Here is my current code:

ActivityViewController class:

-(void)activateIndicator {
NSLog(@"activateIndicator called");
if (isActivated || !delegateView)
    return;
NSLog(@"activateIndicator started");

[delegateView.view setUserInteractionEnabled:NO];
[delegateView.navigationController.view setUserInteractionEnabled:NO];
[delegateView.tabBarController.view setUserInteractionEnabled:NO];

float w = [[UIScreen mainScreen] bounds].size.width;
float h = [[UIScreen mainScreen] bounds].size.height;

NSLog(@"Width = %f\nHeight = %f", w, h);

if (!disabledView) {
    disabledView = [[[UIView alloc] initWithFrame:CGRectMake((w - kNormalWidth) / 2.0, (h - kNormalHeight) / 2.0, kNormalWidth, kNormalHeight)] autorelease];
    disabledView.center = [[[delegateView.view superview] superview] center];
    [disabledView setBackgroundColor:[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.85]];

CALayer *layer = [disabledView layer];
NSLog(@"layer=%@",layer);
NSLog(@"delegate=%@",[layer delegate]);
layer.cornerRadius = 12.0f;
}

if (!activityIndicator) {
    activityIndicator = [[[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(kNormalWidth / 2, 10.0f, 40.0f, 40.0f)] autorelease];
    [activityIndicator setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
    activityIndicator.center = disabledView.center;
}

if (!activityLabel) {
    activityLabel = [[[UILabel alloc] initWithFrame:CGRectMake(10.0f, 100.0f, kNormalWidth - 20, 38)] autorelease];
    activityLabel.text = labelText;
    activityLabel.textAlignment = UITextAlignmentCenter;
    activityLabel.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.0f];
    activityLabel.textColor = [UIColor colorWithWhite:1.0f alpha:1.0f];
    activityLabel.center = disabledView.center;
}

[[[delegateView.view superview] superview] addSubview:disabledView];

[[[delegateView.view superview] superview] addSubview:activityIndicator];
[[[delegateView.view superview] superview] addSubview:activityLabel];


[NSThread detachNewThreadSelector:@selector(startAnimating) toTarget:activityIndicator withObject:nil];
}

Calling Code from multiple places in the app:

    ActivityIndicatorController *aic = [[ActivityIndicatorController alloc] init];
aic.delegateView = self;
aic.labelText = @"Test...";
[aic activateIndicator];

//DO LENGTHY WORK ON MAIN THREAD

[aic deactivateIndicator];
[aic release], aic = nil;
3
You might want to actually add your code ;)Jasarien
Yes, I added it. I had to grab it from my other machine.jschmidt

3 Answers

2
votes

Part 1: I understand that all UI should be handled on the main thread, is that correct?

Correct.

Part 2: What is the difference/advantage/disadvantage of using [NSThread detachThreadSelector] versus NSOperation?

NSOperation is a higher-level interface that allows you to queue operations, create several operations that depend on each other, etc. Other options to work with tasks in the background are performSelectorOnMainThread:.../performSelectorInBackground:... and Grand Central Dispatch.

Part 3: Is it better to:

(a) send the lengthy operation to a new background thread with a callback to the main thread OR

(b) send the UIActivityIndicatorView startAnimating method to a separate thread and run the lengthy process on the main thread

Because of the answer to question 1, (a) is your only option.

1
votes

Put your lengthy work in a separate thread, that way it doesn't completely block out the UI in case you do need some interaction (say to cancel the operation). Then, your ActivityIndicatorController should call out to the main thread to do all the UI stuff, e.g.:

@implementation ActivityIndicatorController

    - (void)activateIndicator
    {
        [self performSelectorOnMainThread:@selector(activateOnMainThread)
                               withObject:nil
                            waitUntilDone:YES];
    }

    - (void)activateOnMainThread
    {
        // Do your actual UI stuff here.
    }

    // And similarly for the deactivate method.
0
votes

Once displayed and animated, the activity indicator will continue animating even the main thread is blocked.

However, the view has no chance to come up because the run loop has yet to executed because pending lengthy operation.

So I think what you need is just performSelector:withObject:afterDelay with delay 0, so your lengthy operation will be queued and performed after your indicator become visible.