How do I get a UIView to appear instantly? - iphone

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.

Related

Perform selector:withObject:afterDelay firing immediately, no delay

I have a very strange error that I have never seen before. I have used this code hundreds of times before, but for some reason, the afterDelay is being completely disregarded.
My Code:
- (void)runIntro
{
if([self introDone])return;
UIImageView* back = [[UIImageView alloc] initWithFrame:[[AppDelegate shared] frame]];
[back setImage:[[AppDelegate shared] bgImage]];
[back setContentMode:UIViewContentModeScaleAspectFill];
[self.view addSubview:back];
UIImageView* target = [[UIImageView alloc] initWithFrame:CGRectMake(([[AppDelegate shared] width]-250)/2.0, ([[AppDelegate shared] height]-350)/2.0, 250, 72)];
[target setImage:[UIImage imageNamed:#"bpside.png"]];
[self.view addSubview:target];
Parabolic* wally = [[Parabolic alloc] initWithFrame:CGRectMake(([[AppDelegate shared] width]-200)/2.0, ([[AppDelegate shared] height]-314)/2.0+250, 200, 200)];
[wally setImage:[UIImage imageNamed:#"wallyside1.png"]];
[self.view addSubview:wally];
[self performSelector:#selector(moveWallyIntro:) withObject:wally afterDelay:1];
[self performSelector:#selector(moveWallyIntro:) withObject:wally afterDelay:2];
}
- (void)moveWallyIntro:(Parabolic*)wally
{
[wally runPBM:50 withDY:300 withDuration:0.5];
}
I know that all of the other code is working properly, but for whatever reason, the selectors are being performed immediately, which causes a major problem. This code is being run at the end of the viewWillAppear method. I have programmed this way many times before and this is the first time I've seen this happen.
UPDATE:
If I remove the code that adds the back UIImageView, this no longer is a problem. How can I add a giant image (3000x2000) in aspect fill format like this so it doesn't cause the program to hang? I'm using the same background image for iphone, ipad, etc., so it needs to be large for the retina iPads...

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];
}];

How to consume UI Events while loading data in the background

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.

UIActivityIndicator question

The apple documentation made this seem like it was pretty easy but it's not working. My code is:
UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[activity startAnimating];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:[NSURL URLWithString:#"http://www.myserver.com"]];
[request setPostValue:name forKey:#"key"];
[request startSynchronous];
NSLog(#"%#",[request responseString]);
[activity stopAnimating];
[activity release];
I didn't set anything up in UIBuilder because frankly, I don't understand exactly how it works with the UIActivityIndicator. Building and Running the app with the code I have above doesnt send off any warning indicators and runs just fine but I am not seeing the activity indicator.
#Eiko's right that your UI is going to block waiting on your synchronous web request to finish, but there's another problem. You're not even adding your activity view to your main view!
You need to give UIActivityIndicatorView *activity a .frame value, and then add it to the view heirarchy. Thusly:
activity.frame = CGRectMake(0,0,50,50); //that'll be top-left corner
[self.view addSubview:activity];
[activity startAnimating];
Now, it'll sit there NOT spinning while you do your web request, because you're doing that request on the main thread. I'm VERY glad you're using ASI, but you're going about it the ugly way.
Make your UIViewController conform to the ASIHttpRequestDelegate protocol.
Set request.delegate = self;.
Implement -requestFinished(ASIHttpRequest *)request to handle the response you get, and in THAT method, hide your activity view. You will PROBABLY wish at that point that you'd made it a named property, or at least an iVar, so you have a handle on it later.
The UI won't update before finishing the method call (well or until the next iteration in the run loop to be more precisely), so this is effectively adding and removing it in the same step.
You should time your request asynchronously to see something happen, or at least schedule the different tasks (adding activity, the request itself and removing it) independently on the main thread. Blocking the main thread is a very bad idea though, and your app will get killed completely if your request takes too long to respond (for whatever reason - easy to think of with web services).
Try putting UIActivityIndicatorView creation in a different thread.
UIView *acty_view=[[UIView alloc]initWithFrame:CGRectMake(130,220, 60, 60)];
acty_view.backgroundColor=[UIColor blackColor];
acty_view.alpha = 0.6;
acty_view.layer.cornerRadius = 10;
acty_view.layer.masksToBounds = YES;
activitidicator = [[UIActivityIndicatorView alloc]initWithFrame:CGRectMake(15, 15, 30, 30)];
[activitidicator setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhite];
[activitidicator setColor:[UIColor whiteColor]];
UILabel *lbl=[[UILabel alloc]initWithFrame:CGRectMake(8, 42, 100, 15)];
lbl.text=#"Loading";
[lbl setFont:[UIFont systemFontOfSize:10]];
lbl.textColor = [UIColor whiteColor];
// [lbl setFont:[UIFont fontWithName:#"Helvetica" size:13]];
[acty_view addSubview:lbl];
[acty_view addSubview:activitidicator];
[self.view addSubview:acty_view];
[activitidicator startAnimating];
[self performSelector:#selector(CallWebserviceMethod) withObject:activitidicator afterDelay:0];`
-(void)CallWebserviceMethod
{
[self JsonResults];
[activitidicator stopAnimating];
}

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.