How to consume UI Events while loading data in the background - iphone

I have a little iPhone app that loads data from a web service. To make sure that nothing goes wrong while loading the data I create a semi-transparent view over the app and use CFRunloopRun() to wait until all the data is loaded in the background. This is the code for that:
self.connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self] autorelease];
// Now show an animation
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
UIView *window = [[UIApplication sharedApplication] keyWindow];
UIView *shield = [[UIView alloc] initWithFrame:window.bounds];
shield.backgroundColor = [UIColor blackColor];
shield.alpha = 0.5f;
[window addSubview:shield];
spinner.center = shield.center;
[shield addSubview:spinner];
spinner.hidden = NO;
NSLog( #"JCL.callServerWithRequest(), spinner view: %#, shield view: %#, window: %#", spinner, shield, window );
[spinner startAnimating];
// Hand over to the Runnloop to wait
CFRunLoopRun();
[spinner stopAnimating];
[spinner removeFromSuperview];
[spinner release];
[shield removeFromSuperview];
[shield release];
This works fine except that any clicks on a button somewhere is played after the loading so if the users clicks on the download button twice he will do the download twice as well.
Any idea how to consume the UI events before the shield is removed.
Thanks - Andy

Try it without messing with runloops. I suspect that the UI events are coming in to the normal loop on the window but not being processed until your custom loop returns, at which point the "shield" view is no longer there to catch them. If you put the shield in place and then let the main runloop handle things, the shield should catch them all as normal.

Thanks to Anomie I finally tried out to go without the CFRunLoopRun() and it is quite difficult because the execution is split into two parts: - the Invocation and the Return of the Result through a callback. But then I shot myself in the proverbial foot because I tried to block the returning thread to slow down the execution which did not work because that was executed again in the main thread.
Eventually I did slow down the Web Service and then everything worked as expected.

Related

How to refresh display before returning to run loop

I am trying to display a message in the foreground by calling drawRect: or by adding a view that appears in foreground. The message must be displayed after pressing a button. I have noticed that the message is displayed only if the function that is called when button is pressed had exited.
- (IBAction)testBtnPress:(id)sender {
TestView *testView = [[TestView alloc] initWithFrame:self.view.frame];
[self.view addSubview:testView];
// processing to be done while the message is displayed
}
The drawRect: is called in the TestView class.
The rectangle with message is displayed after the processing to be done is finished, which is not convenient for my application.
Is there a way to display the message while the processing in running (the message will be "Processing... Please Wait").
Thanks.
The processing must either be done in a background thread, or performed in the next iteration of the runloop, to give your UI changes time to be handled.
You can do the processing in the next iteration of the runloop with something like this:
TestView *testView = ... ;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// processing to be done while the message is displayed
}];
This should work:
dispatch_async(dispatch_get_main_queue(), ^{
TestView *testView = [[TestView alloc] initWithFrame:self.view.frame];
[self.view addSubview:testView];
});

Having issue updating hud ivar during in-app purchase

I have recently set-up an in-app purchase mechanism in my app. During the purchase i would like to update an hud (i am using the mbprogresshud) according to two kinds of events: i start a purchase and i receive a validation of the purchase. The problem i am facing is that the hud is never updated with the one i want when the purchase is done (custom view):
When i click on the purchase button:
-(IBAction)buyButtonTapped:(id)sender {
self.hud = [[SCLProgressHUD alloc] initWithView:self.view];
[self.view addSubview:self.hud];
self.hud.labelText = #"Connecting...";
self.hud.minSize = CGSizeMake(100 , 100);
[self.hud show:YES];
...
}
When i receive a notification that the purchase was successful:
-(void)productPurchased:(NSNotification *)notification {
self.hud.customView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkmark_icon.png"]];
self.hud.mode = SCLProgressHUDModeCustomView;
self.hud.labelText = #"Thanks for your purchase!";
...
}
Setting the self.hud.customView property in the last method will trigger a [self setNeedsLayout]; and [self setNeedsDisplay] within the hud class but still i don't observe any change.
Any possible idea of what i am doing wrong here?
As mentioned on the mbprogress hud readme, update of the UI when tasks are performed on the main thread required a slight delay to take effect. What happen in my case is that the hud was a strong property of a popover controller that i dismiss immediately so i didn't get a chance to see the update happening. I now dismiss the controller in the completion block of:
-(void)showAnimated:(BOOL)animated
whileExecutingBlock:(dispatch_block_t)block completionBlock:(void
(^)())completion
And my code snippet look like like this for the dismiss:
[_hud showAnimated:YES whileExecutingBlock:^(void){
[self.successPurchaseSoundEffect play];
_hud.customView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkmark_icon.png"]];
_hud.mode = SCLProgressHUDModeCustomView;
_hud.labelText = #"Thanks!";
// We need to pause the background thread to let the music play and the hud be updated
sleep(1);}
completionBlock:^(void){
[self.delegate dismissPurchaseInfoController:self];
}];

UIActivityIndicatorView unresponsive

When a UIButton is triggered, a UIActivityIndicator is started, and then stopped when implementLogin finishes:
-(IBAction)loginButton {
NSLog(#"loginButton triggered");
// Checks the Fields are not empty
if ([sessionIdField.text length] != 0 && [usernameField.text length] != 0 ) {
NSLog(#"Beginning Spinner");
// Displays spinner
[activitySpinner startAnimating];
[self implementLogin];
// Displays spinner
[activitySpinner stopAnimating];
}
}
However at runtime, the spinner doesn't appear! I have set the spinner to 'hide when stopped'.
When I set the activity indicator to animate before the view loads, it appears as it should, so I'm guessing it has a problem with the UIButton... (Also, I'm using 'Touch Up Inside' for the button.)
It's a simple problem... Can anyone help?
Thanks
Whatever implementLogin is doing (making a network request, perhaps?), it's doing it on the main thread, which is likely blocking UI updates like spinner animation.
You could recode something like this:
[activitySpinner startAnimating];
dispatch_async(dispatch_get_global_queue(DISPATCH_PRIORITY_DEFAULT, 0), ^{
[self implementLogin];
dispatch_async(dispatch_get_main_queue(), ^{
// Stops spinner
[activitySpinner stopAnimating];
}
}
[Code is untested, but you get the idea.]
What is happening here is that you are dispatching the login task to the background, and the last thing that block will do is stop the spinner on the main thread (as a separate task).
Without more code, one can only guess the reason why it doesn't work.
I assume 'Beginning Spinner' output correctly(?)
If so, you probably didn't properly initialize the UIActivityIndicatorView.
Does it look like this?
activitySpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
activitySpinner.hidesWhenStopped = YES;
[view addSubview:activitySpinner];

How do I get a UIView to appear instantly?

I'm trying to create an activity indicator in iPhone app. The problem is that I cannot get it to appear before the actual task i want it to diplay during is already done. Is there something funky about the order in which the iPhone does stuff?
Here is my problematic code (in my app delegate):
-(BOOL)showProgressView: (NSString *) message {
self.progress = [[UIView alloc] initWithFrame:window.frame];
UIImageView *img = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"squircle.png"]];
[img setAlpha:0.5];
[img setFrame:CGRectMake(94, 173, 133, 133)];
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(51.5, 51.5, 30, 30)];
spinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
[img addSubview:spinner];
[self.progress addSubview:img];
[spinner startAnimating];
[img release];
[spinner release];
[window addSubview:self.progress];
return YES;
}
I then call this code like this:
if ([appDelegate showProgressView:#"Loading..:"])
{
//My actual code loads data and stuff here but that is not important
//drawCtrl is a UIViewController subclass that is instantiated here
UINavigationController *navController = [appDelegate navigationController];
[navController pushViewController:drawCtrl animated:YES];
[drawCtrl release];
}
The problem is that my activity indicator does not appear until the new view controller is pushed onto the navController's stack. Can I control this in some way?
Thanks in advance!
-Mats
You need to call your loading method periodically via a timer. UI changes require a trip through the event loop to be seen. I have done this usually in a timer I set up in the AppDelegate, it calls the "methodThatTakesSomeTime" every n seconds, the "methodThatTakesSomeTime" method does some work for a specified time slice and then exits which is usually about the same as the timer trigger time. This gives the event loop time to refresh the UI and also give your method time to do its stuff.
Takes some fiddling with keeping the state of the "methodThatTakesSomeTime" so it can continue on its way, but that is what it takes.

view hierarchy refresh timing

I'm trying to add a progress meter, or other "I'm busy right now" notification to my view hierarchy right before doing some intense computation that will block the UI. My code looks some thing like:
//create view
[currentTopView addSubView:imBusyView];
//some initialization for the intense computation
[computation startComputing];
Unfortunately, my progress meter doesn't display until after the computation completes. It appears like the views aren't re-drawn until the run loop completes. I'm pretty sure that setNeedsDisplay and setNeedsLayout will also wait until the run loop completes.
How do I get the view to display immediately?
Redrawing only occurs when your code returns control to the run loop. So the easiest way would be for you to schedule the startComputing call with a zero delay. That way, it will be executed during the next run loop iteration (right after redrawing):
[computation performSelector:#selector(startComputing) withObject:nil afterDelay:0.0];
Be aware, though, that unless you do your computation in another thread you will not be able to update the UI during the computation.
If you are doing heavy calculations maybe spawning a new thread is a good idea.
Here I have an activityIndicator displayed and starts a large XMLParse operation in a background thread:
- (void) setSearchParser {
activityIndicator = [[ActivityIndicatorView alloc] initWithActivity];
[self.view addSubview:activityIndicator];
[NSThread detachNewThreadSelector:#selector(getSearchResults:) toTarget:self withObject:[searchParser retain]];
}
then the getSearchResults method:
- (void) getSearchResults: (SearchResultParser *) parser {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
[parser startParser];
[self performSelectorOnMainThread:#selector(searchResultsReady:) withObject:[parser data] waitUntilDone:NO];
[pool release];
}
So firstly make a new thread:
[NSThread detachNewThreadSelector:#selector(getSearchResults:) toTarget:self withObject:[searchParser retain]];
this means that all code inside the getSearchResults will be executed on a different thread. getSearchResults also get's passed a parameter of "searchParser" thats a large object that just needs startParse called on it to begin.
This is done in getSearchResults. When the [parser startParser] is done, the results is passed back to the main thread method called "searchResultsReady" and the threads autorelease pool is released.
All the time it took from my parser began to it had finished, a gray view covered the screen an an activityIndicator ran.
You can have the small activityIndicator class I wrote:
-(id) initWithActivity {
[self initWithFrame:[self bounds]];
[self setBackgroundColor:[UIColor blackColor]];
[self setAlpha:0.8];
activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activityView.center = CGPointMake(160, 240);
[self addSubview:activityView ];
[activityView startAnimating];
return self;
}
- (void) dealloc {
[activityView release];
[super dealloc];
}
Hope it helps you out, even though threads seems a bit confusing, they can help to make the UI not freeze up, which is especially important on the iPhone.