I tried to create a SplashView which display the Default.png in the background and a UIProgressBar in front. But the splash screen is not being updated...
Inside my view controller I load first the splash view with a parameter how many steps my initialisation has and then I start a second thread via NSTimer and after each initialisation step I tell the SplashView to display the new progress value.
All looks good in theory, but when running this app the progress bar is not being updated (the method of the splash screen receives the values, I can see it in the logs). I also tried to add usleep(10000); in between to give the view updates a bit time and also instead of using the progress bar I drew directly on the view and called [self setNeedsDisplay]; but all didn't work :/
What am I doing wrong?
Thanks for your help!
Tom
Here is some code:
SPLASHSCREEN:
- (id)initWithFrame:(CGRect)frame withStepCount:(int)stepCount {
if (self = [super initWithFrame:frame]) {
// Initialization code
background = [[UIImageView alloc] initWithFrame: [self bounds]];
[background setImage: [UIImage imageWithContentsOfFile: [NSString stringWithFormat:#"%#/%#", [[NSBundle mainBundle] resourcePath], #"Default.png"]]];
[self addSubview: background];
progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar];
[progressView setFrame:CGRectMake(60.0f, 222.0f, 200.0f, 20.0f)];
[progressView setProgress: 0.0f];
stepValue = 1.0f / (float)stepCount;
[self addSubview:progressView];
}
return self;
}
- (void)tick {
value += stepValue;
[progressView setProgress: value];
}
VIEWCONTROLLER:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
splashView = [[SplashView alloc] initWithFrame: CGRectMake(0.0f, 0.0f, 320.0f, 480.0f) withStepCount:9];
[self setView: splashView];
NSTimer* delayTimer;
delayTimer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(finishInitialization) userInfo:nil repeats:NO];
}
return self;
}
- (void)finishInitialization {
// do some stuff, like allocation, opening a db, creating views, heavy stuff...
[splashView tick]; // this should update the progress bar...
// do some stuff, like allocation, opening a db, creating views, heavy stuff...
[splashView tick]; // this should update the progress bar...
// init done... set the right view and release the SplashView
}
As mentioned in another answer, for some finite amount of time, as your app is being launched, Default.png is displayed and you have no control over it. However, if in your AppDelegate, you create a new view that displays the same Default.png, you can create a seamless transition from the original Default.png to a view that you can add a progress bar to.
Now, presumably, you have created a view or similar and you are updating a progress bar every so often in order to give the user some feedback. The challenge here is that your view is only drawn when it gets called to do a drawRect. If, however, you go from AppDelegate to some initialization code to a viewcontroller's viewDidLoad, without the run loop getting a chance to figure out which views need to have drawRect called on, then your view will never display its status bar.
Therefore in order to accomplish what you want, you have to either make sure that drawRect gets called, such as by pushing off a lot of your initialization code into other threads or timer tasks, or you can force the drawing by calling drawRect yourself, after setting up contexts and such.
If you go with the background tasks, then make sure your initialization code is thread-safe.
Default.png is just a graphic, a static image shown while the application is launching. If you want to show further progress, you'll have to show everything at the applicationDidLaunch phase. Show your modal "Splash Screen" there first (Create a view controller, add its view as a subview of your main window) and dismiss it when you are done whatever additional loading you needed to do.
Also, you need to do update your progress bar in a seperate thread. Updating your GUI in the same thread where a lot of business is going on is (in my opinion, but I could be wrong) a bad idea.
The main thread is, as far as I know, the only one that can safely do GUI things, and its event loop (that is, the main application thread's) is the one that does the actual displaying after you've called -setNeedsDisplay. Spawn a new thread to do your loading, and update the progress on the main thread.
Related
I've made a custom zero-image, Core Graphics drawn UIButton based on Ray Wenderlich's Tutorial (the only modifications are to CoolButton's drawRect: method, altered drawing code), and it works great most of the time. However, sometimes when I click it for a short period of time, it stays in a depressed state and doesn't return to normal.
From here, the only way to get it back to a normal state is via a long press. Simply clicking means it stays depressed.
Another thing to note is that I've hooked Touch Up Inside up to a chain of a few long methods - I don't think it would take more than 0.1 seconds to complete. I've even used dispatch_async in the #selector that is hooked up to Touch Up Inside, so there shouldn't be a delay in the UI updating, I think.
I've put an NSLog in the drawRect: which fires 3 times per button press usually, and it varies what UIControlState the button is in for each press:
For some short presses, it goes Highlighted, Highlighted, Normal
for longer presses, it's Highlighted, Normal, Normal
However, for very short presses, it only fires twice, Highlighted -> Highlighted.
When it's a long press to get it back to Normal, it goes H, N, N.
This has been puzzling me for a while, and I haven't been able to work out why short presses only fire drawRect: twice, or why touchesEnded: doesn't seem to call drawRect:. Perhaps touchesEnded: isn't firing?
I really hope someone can help.
If you really want to generate the button images at runtime, generate them when the button is loaded. Then add them for different states using
[button setImage:btnImage forState:UIControlStateNormal];
You can always turn a view into an image using the following: http://pastie.org/244916
Really though, I'd recommend just making images beforehand. If you don't want to get photoshop, there's plenty of alternatives. The upcoming pixelmator update looks pretty suave, ands it's ridiculously cheap!
Well, well, that was easy. 0.15 second delay works, 0.1 doesn't.
As other said, you can simply generate an image and use that as background (if the image is static). No need photoshop, you may simply generate your image once and then take a snapshot, then cut the image out (with Anteprima) and save it as a png file :)
However, since you said the button is connected to some long methods, this may be why the button stay pressed: if you are not calling those methods in background, then the button will stay pressed since all the task are ended. Try (for a test) to connect the button with a single simple method (say NSLog something) and check if it stay pressed. If not, I suggest to detach your methods in background.
I too ran into a similar problem with a UIButton appearing to stick one state when I customize the drawRect method. My hunch is that the state of the button is changing more than once before drawRect is called. So I just added a custom property that would only be set by methods called during touchDown and touchUpInside events. I had to dispatch threads to do this because of other operations that were holding up the main thread, causing a delay in the redrawHighlighted method.
It may be a little messy, but here's what worked for me:
// CustomButton.h
#interface CustomButton : UIButton
#property (atomic) BOOL drawHighlighted;
// CustomButton.m
#implementation CustomButton
#synthesize drawHighlighted = _drawHighlighted;
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
_drawHighlighted = NO;
[self addTarget:self action:#selector(redrawHighlighted) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:#selector(redrawNormal) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:#selector(redrawNormal) forControlEvents:UIControlEventTouchDragExit];
}
return self;
}
- (void)drawRect:(CGRect)frame
{
// draw button here
}
- (void)redrawNormal
{
[NSThread detachNewThreadSelector:#selector(redrawRectForNormal) toTarget:self withObject:nil];
}
- (void)redrawHighlighted
{
[NSThread detachNewThreadSelector:#selector(redrawRectForHighlighted) toTarget:self withObject:nil];
}
- (void)redrawRectForNormal
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
_drawHighlighted = NO;
[self setNeedsDisplay];
[pool release];
}
- (void)redrawRectForHighlighted
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
_drawHighlighted = YES;
[self setNeedsDisplay];
[pool release];
}
Another cheap but effective alternative I've found is to create a drawRect: that doesn't do any highlighting, and simply alter the button subclass's alpha property when the user interacts with the button.
For example:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
self.alpha = 0.3f;
return [super beginTrackingWithTouch:touch withEvent:event];
}
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
self.alpha = 1.0f;
[super endTrackingWithTouch:touch withEvent:event];
}
- (void)cancelTrackingWithEvent:(UIEvent *)event
{
self.alpha = 0.3f;
[super cancelTrackingWithEvent:event];
}
- (void)drawRect:(CGRect)rect
{
// Your drawing code here sans highlighting
}
I'd like to display an activity indicator BEFORE the work undertaken by willAnimateRotationToInterfaceOrientation:duration: begins. Most of the time in my app, this work is quickly completed and there would be no need for an activity indicator, but occasionally (first rotation, i.e. before I have cached data, when working with a large file) there can be a noticeable delay. Rather than re-architect my app to cope with this uncommon case, I'd rather just show the UIActivityIndicatorView while the app generates a cache and updates the display.
The problem is (or seems to be) that the display is not updated between the willRotateToInterfaceOrientation:duration and the willAnimateRotationToInterfaceOrientation:duration: method. So asking iOS to show UIActivityIndicator view in willRotate method doesn't actually affect the display until after the willAnimateRotation method.
The following code illustrates the issue. When run, the activity indicator appears only very briefly and AFTER the simulateHardWorkNeededToGetDisplayInShapeBeforeRotation method has completed.
Am I missing something obvious? And if not, any smart ideas as to how I could work around this issue?
Update: While suggestions about farming the heavy lifting off to another thread etc. are generally helpful, in my particular case I kind of do want to block the main thread to do my lifting. In the app, I have a tableView all of whose heights need to be recalculated. When - which is not a very common use case or I wouldn't even be considering this approach - there are very many rows, all the new heights are calculated (and then cached) during a [tableView reloadData]. If I farm the lifting off and let the rotate proceed, then after the rotate and before the lifting, my tableView hasn't been re-loaded. In the portrait to landscape case, for example, it doesn't occupy the full width. Of course, there are other workarounds, e.g. building a tableView with just a few rows prior to the rotate and then reloading the real one over that etc.
Example code to illustrate the issue:
#implementation ActivityIndicatorViewController
#synthesize activityIndicatorView = _pgActivityIndicatorView;
#synthesize label = _pgLabel;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
[self hideActivityIndicatorView];
}
- (void) simulateHardWorkNeededToGetDisplayInShapeBeforeRotation;
{
NSLog(#"Starting simulated work");
NSDate* date = [NSDate date];
while (fabs([date timeIntervalSinceNow]) < 2.0)
{
//
}
NSLog(#"Finished simulated work");
}
- (void) showActivityIndicatorView;
{
NSLog(#"showActivity");
if (![self activityIndicatorView])
{
UIActivityIndicatorView* activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self setActivityIndicatorView:activityIndicatorView];
[[self activityIndicatorView] setCenter:[[self view] center]];
[[self activityIndicatorView] startAnimating];
[[self view] addSubview: [self activityIndicatorView]];
}
// in shipping code, an animation with delay would be used to ensure no indicator would show in the good cases
[[self activityIndicatorView] setHidden:NO];
}
- (void) hideActivityIndicatorView;
{
NSLog(#"hideActivity");
[[self activityIndicatorView] setHidden:YES];
}
- (void) dealloc;
{
[_pgActivityIndicatorView release];
[super dealloc];
}
- (void) viewDidLoad;
{
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(50.0, 50.0, 0.0, 0.0)];
[label setText:#"Activity Indicator and Rotate"];
[label setTextAlignment: UITextAlignmentCenter];
[label sizeToFit];
[[self view] addSubview:label];
[self setLabel:label];
[label release];
}
#end
The app doesn't update the screen to show the UIActivityIndicatorView until the main run loop regains control. When a rotation event happens, the willRotate... and willAnimateRotation... methods are called in one pass through the main run loop. So you block on the hard work method before displaying the activity indicator.
To make this work, you need to push the hard work over to another thread. I would put the call to the hard work method in the willRotate... method. That method would call back to this view controller when the work is completed so the view can be updated. I would put show the activity indicator in the willAnimateRotation... method. I wouldn't bother with a didRotateFrom... method. I recommend reading the Threaded Programming Guide.
Edit in response to a comment: You can effectively block user interaction by having the willAnimateRotation... method put a non functioning interface on screen such as a view displaying a dark overlay over and the UIActivityIndicatorView. Then when the heavy lifting is done, this overlay is removed, and the interface becomes active again. Then the drawing code will have the opportunity to properly add and animate the activity indicator.
More digging (first in Matt Neuberg's Programming iPhone 4) and then this helpful question on forcing Core Animation to run its thread from stackoverflow and I have a solution that seems to be working well. Both Neuberg and Apple issue strong caution about this approach because of the potential for unwelcome side effects. In testing so far, it seems to be OK for my particular case.
Changing the code above as follows implements the change. The key addition is [CATransaction flush], forcing the UIActivityIndicatorView to start displaying even though the run loop won't be ended until after the willAnimateRotationToInterfaceOrientation:duration method completes.
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
[CATransaction flush]; // this starts the animation right away, w/o waiting for end of the run loop
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
[self hideActivityIndicatorView];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
}
Try performing you work on a second thread after showing the activity view.
[self showActivityIndicatorView];
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.01];
Either execute the heavy lifting in a background thread and post the results in the foreground thread to update the UI (UIKit is only thread safe since iOS 4.0):
[self performSelectorInBackground:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil]
Or you can schedule the heavy lifting method to be executed after the rotation took place:
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.4]
But these are only hacks and the real solution is to have proper background processing if your UI needs heavy processing to get updated, may it be in portrait or landscape. NSOperation and NSOperationQueue is a good place to start.
In my iPhone app, I want to add activity indicator on top of a searchbar.
When it is searching it should display activity indicator.
I have added the activity indicator in XIB and created its outlet.
I am making it hide when the searching finishes, but Activity Indicator does not display.
Problem
I figured out that search function(say A)(where I animate the activity indicator) in turn calls another function(say B) so the main thread is being used in executing the function B. But for activity indicator to animate we require the main thread.
So I tried calling function B using performSelectorInBackGround:withObject method. Now when I click search the activity indicator is shown but the functionality of function B does not execute.
What can be a work-around for this?
There is not quite enough in your question to go on, but to start debugging, I would do the following.
Verify that the activity variably is really wired to the UIActivityIndicator you are creating in IB. (I would set a breakpoint on the setHidden: lines and make sure the variable is not null. Or throw an NSAssert(activity,#"Whoops! actity is null"); in there.)
If the variable is indeed set, I would start checking that it is in the right place in the view hierarchy. (I'd try doing a [self.view addSubview:activity] and see that it appears. You might have to replace it somewhere else.)
You might also want to try having it on by default in IB, until you have everything figured out.
Good Luck. Hope this helps.
Save yourself the hassle of creating a custom activity indicator and use the standard one that's available for you already - in the top bar. Also, IMO users tend to expect that one to spin when something is happening.
UIApplication* app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = YES;
Obviously, set it to NO when your activity is over.
First of all, make sure you have #synthesize activity at the top of your .m file. Then in the viewDidLoad method, type activity.hidesWhenStopped = TRUE;. Next, in the method that is called when the search starts, type [activity startAnimating]; and [activity stopAnimating]; in the method when the searching stops.
try this:
set hidesWhenStopped = NO, so that is displayed all the time and then hide and show it manually. But the View should be set in IB to hidden first.
- (void)startActivityView {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
activity_view.hidden = NO;
[pool drain];
}
- (void)stopActivityView {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
activity_view.hidden = YES;
[pool drain];
}
- (void)doSomething {
[self performSelectorInBackground:#selector(startActivityView) withObject:nil];
// do some time consuming work
[self performSelectorInBackground:#selector(stopActivityView) withObject:nil];
}
Perhaps you have a view in front of your activity indicator? What if you always bring it to the front....
loadView = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
loadView.frame = CGRectMake(0.0, 0.0, 40.0, 40.0);
loadView.center = window.center;
loadView.opaque = NO;
[window addSubview: loadView];
[window bringSubviewToFront:loadView];
[loadView startAnimating];
I suggest that you use DSActivityView for showing your activity indicator. The source code can be found at Dejal blog.
Showing, and hiding, the activity view is a simple line of code.
[DSActivityView activityViewForView:self.view];
start animating the activity indicator and with a delay of 0.1 or 0.2 just call the other method u want.... i tried and it is working for me....
I have got the solution and it is as follows.
I just wrote the below line in Search button click event.
[NSThread detachNewThreadSelector:#selector(threadStartAnimating:) toTarget:self withObject:nil];
And defined the function threadStartAnimating: as follows:
-(void)threadStartAnimating:(id)data
{
[activityIndicator setHidden:NO];
[activityIndicator startAnimating];
}
I have a view (splash screen) which displays for two minutes:
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
[viewController showSplash];
}
- (void)showSplash // Show splash screen
{
UIViewController *modalViewController = [[UIViewController alloc] init];
modalViewController.view = modelView;
[self presentModalViewController:modalViewController animated:NO];
[self performSelector:#selector(hideSplash) withObject:nil afterDelay:120.0];
}
I want to add a timer which counts down from 2 minutes to zero to this splash screen.
I imagine I will need to create another view containing the timer. Is this correct? How would I do this and add it to the splash screen, and how would I make the numbers in the timer be displayed on screen in white?
I know two minutes is a very long time to display a splash screen for... but I am just experimenting with various things, there are other things going on for the two minutes!
Many thanks,
Stu
Edit // Ok I now have this:
(.h)
NSTimer *timer5;
UILabel *countDown;
float timeOnSplash;
(.m)
- (void) updateLabel:(NSTimer*)theTimer
{
float timeOnSplash = timeOnSplash - 1;
countDown.text = [NSString stringWithFormat:#"%02d:%02d", timeOnSplash];
}
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
timer5 = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:#selector(updateLabel:)
userInfo:nil
repeats:YES];
countDown.text = [NSString stringWithFormat:#"%02d:%02d", timeOnSplash];
}
I get the following uncaught exception when I run the code:
'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key countDown.'
Any ideas?
Edit 2 // Working now, for an excellent solution see TechZen's answer.
Many thanks to all!
NSTimers are unusual objects in that they don't attach to the object that creates them but to the applications NSRunLoop instances. If a timer is one shot, you don't have to retain any reference to it. You should start the timer and forget about it.
In your case you need two track two time intervals (1) the passing of each second so you can update the interface and (2) passing of the two minute interval total.
For (1) you should evoke a repeating timer of one second interval that calls a method in the modalviewcontroller that updates the interface. The best place to evoke the timer would be in the controlller's viewDidAppear method. For (2) you can have property of the controller that stores a value of 159 and then have the method called by the timer decrement it each time the method is called. When it reaches zero, it invalidates the timer.
You should be aware that timers are affected by how quickly the runloop processes all events. If you have intensive background processes that don't pause every few microseconds, the timer may fail to fire on time. If you run into this problem, you should consider creating a separate threads for the splash screen and the configuration.
I do have to wonder why you need display the splash screen for exactly two minutes.
As an aside, the iPhone Human Interface Guidelines expressly state that you should not use splash screens. They can cause your app to be rejected. Using a splash screen that hangs around to long gives the impression that the app or the phone as failed and Apple doesn't like that either.
If you have some heavy duty configuration to do before the app is usable, it is better to create an interface that shows the configuration in process. That way, it is clear that the app is working and not just hung.
Even better, because no one on the move wants to stare at a static iPhone app for two minutes, it's better to get your user started doing something in one thread while the app configures in another. For example, in some kind of url connection, you could start the user typing in some and address of some data while the app makes the connection. For a game, you could have the user select their user name, review high scores, view instructions etc.
You should remember in the design process that people use apps on the iPhone primarily because it saves them time. They don't have drag out a laptop or go to a computer to perform some task. Your app design should focus on getting the user's task performed as quickly as possible. That is true even in the case of a game.
Normally, I would warn against premature optimization but this is kind of big deal.
Edit01:
You want something like this in your splash screen controller.
#interface SplashScreenViewController : UIViewController {
NSInteger countDown;
IBOutlet UILabel *displayTimeLabel;
}
#property NSInteger countDown;
#property(nonatomic, retain) UILabel *displayTimeLabel;
#end
#import "SplashScreenViewController.h"
#implementation SplashScreenViewController
#synthesize countDown;
#synthesize displayTimeLabel;
-(void) viewDidAppear:(BOOL)animated{
countDown=120;
NSTimer *secTimer=[NSTimer timerWithTimeInterval:1.0 target:self selector:#selector(updateCountDown:) userInfo:nil repeats:YES];
}//------------------------------------viewDidAppear:------------------------------------
-(void) updateCountDown:(NSTimer *) theTimer{
NSInteger mins,secs;
NSString *timeString;
countDown--;
if (countDown>=0) {
mins=countDown/60;
secs=countDown%60;
displayTimeLabel.text=[NSString stringWithFormat:#"%02d:%02d",mins,secs];
} else {
[theTimer invalidate];
// do whatever you wanted to do after two minutes
}
}//-------------------------------------(void) updateCountDown------------------------------------
#end
-------
you can call some method every second with NSTimer. there you can change view of your modal ViewController(for example, change text on the label, which will show time). and when your method will be called for the 120 time, you just invalidate your timer
You can use an NSTimer as #Morion suggested. An alternative is the following:
In your #interface file, add the variable:
NSInteger countDown;
then in #implementation:
- (void)showSplash // Show splash screen
{
UIViewController *modalViewController = [[UIViewController alloc] init];
modalViewController.view = modelView;
[self presentModalViewController:modalViewController animated:NO];
countdown = 120;
[self performSelector:#selector(updateTime) withObject:nil afterDelay:1.0];
}
- (void)updateTime {
//decrement countDown
if(--countDown > 0){
//
// change the text in your UILabel or wherever...
//
//set up another one-second delayed invocation
[self performSelector:#selector(updateTime) withObject:nil afterDelay:1.0];
}else{
// finished
[self hideSplash];
}
}
Yes, one way would be to create a new view which has a transparent background ([UIColor clearColor]) and a label with white text. Just call [modalViewController.view addSubview:timerView] to add it as a subview/overlay.
I'm not sure how much help you need with actually creating the views and setting up the label etc.
I am totally stumped, here's the situation:
My app uses the Core Location framework to get the current location of the user and then pings my server at TrailBehind for interesting places nearby and displays them as a list. No problems.
To conserve batteries, I turn off the GPS service after I get my data from the server. If the user moves around while using the app and wants a new list he clicks "Refresh" on the navigation controller and the CLLocation service is again activated, a new batch of data is retrieved from the server and the table is redrawn.
While the app is grabbing data from my server I load a loading screen with a spinning globe that says "Loading, please wait" and I hide the navigation bar so they don't hit "back".
So, the initial data grab from the server goes flawlessly.
The FIRST time I hit refresh all the code executes to get a new location, ping the server again for a new list of data and updates the cells. However, instead of loading the table view as it should it restores the navigation controller bar for the table view but still shows my loading view in the main window. This is only true on the device, everything works totally fine in the simulator.
The SECOND time I hit refresh the function works normally.
The THIRD time I hit refresh it fails as above.
The FOURTH time I hit refresh it works normally.
The FIFTH time I hit refresh it fails as above.
etc etc, even refreshes succeed and odd refreshes fail. I stepped over all my code line by line and everything seems to be executing normally. I actually continued stepping over the core instructions and after a huge amount of clicking "step over" I found that the table view DOES actually display on the screen at some point in CFRunLoopRunSpecific, but I then clicked "continue" and my loading view took over the screen.
I am absolutely baffled. Please help!! Many thanks in advance for your insight.
Video of the strange behavior:
Relevant Code:
RootViewControllerMethods (This is the base view for this TableView project)
- (void)viewDidLoad {
//Start the Current Location controller as soon as the program starts. The Controller calls delegate methods
//that will update the list and refresh
[MyCLController sharedInstance].delegate = self;
[[MyCLController sharedInstance].locationManager startUpdatingLocation];
lv = [[LoadingViewController alloc] initWithNibName:#"Loading" bundle:nil];
[self.navigationController pushViewController:lv animated:YES];
[super viewDidLoad];
}
- (void)updateClicked {
//When the location is successfully updated the UpdateCells method will stop the CL manager from updating, so when we want to update the location
//all we have to do is start it up again. I hope.
[[MyCLController sharedInstance].locationManager startUpdatingLocation];
[self.navigationController pushViewController:lv animated:YES];
//LV is a class object which is of type UIViewController and contains my spinning globe/loading view.
}
-(void)updateCells {
//When the Core Location controller has updated its location it calls this metod. The method sends a request for a JSON dictionary
//to trailbehind and stores the response in the class variable jsonArray. reloadData is then called which causes the table to
//re-initialize the table with the new data in jsonArray and display it on the screen.
[[MyCLController sharedInstance].locationManager stopUpdatingLocation];
if(self.navigationController.visibleViewController != self) {
self.urlString = [NSString stringWithFormat:#"http://www.trailbehind.com/iphone/nodes/%#/%#/2/10",self.lat,self.lon];
NSURL *jsonURL = [NSURL URLWithString:self.urlString];
NSString *jsonData = [[NSString alloc] initWithContentsOfURL:jsonURL];
NSLog(#"JsonData = %# \n", jsonURL);
self.jsonArray = [jsonData JSONValue];
[self.tableView reloadData];
[self.navigationController popToRootViewControllerAnimated:YES];
[jsonData release];
}
}
CLController Methods: Basically just sends all the data straight back to the RootViewController
// Called when the location is updated
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
NSLog(#"New Location: %# \n", newLocation);
NSLog(#"Old Location: %# \n", oldLocation);
#synchronized(self) {
NSNumber *lat = [[[NSNumber alloc] init] autorelease];
NSNumber *lon = [[[NSNumber alloc] init] autorelease];
lat = [NSNumber numberWithFloat:newLocation.coordinate.latitude];
lon = [NSNumber numberWithFloat:newLocation.coordinate.longitude];
[self.delegate noteLat:lat];
[self.delegate noteLon:lon];
[self.delegate noteNewLocation:newLocation];
[self.delegate updateCells];
}
}
The first thought is that you may not want to send startUpdatingLocation to the CLLocationManager until after you've pushed your loading view. Often the first -locationManager:didUpdateToLocation:fromLocation: message will appear instantly with cached GPS data. This only matters if you're acting on every message and not filtering the GPS data as shown in your sample code here. However, this would not cause the situation you've described - it would cause the loading screen to get stuck.
I've experienced similarly weird behavior like this in a different situation where I was trying to pop to the root view controller when switching to a different tab and the call wasn't being made in the correct place. I believe the popToRootViewController was being called twice for me. My suspicion is that your loading view is either being pushed twice or popped twice.
I recommend implementing -viewWillAppear:, -viewDidAppear:, -viewWillDisappear: and -viewDidDisappear: with minimal logging in your LoadingViewController.
- (void)viewWillAppear:(BOOL)animated {
NSLog(#"[%# viewWillAppear:%d]", [self class], animated);
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
NSLog(#"[%# viewDidAppear:%d]", [self class], animated);
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
NSLog(#"[%# viewWillDisappear:%d]", [self class], animated);
[super viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated {
NSLog(#"[%# viewDidDisappear:%d]", [self class], animated);
[super viewDidDisappear:animated];
}
Then, run a test on your device to see if they are always being sent to your view controller and how often. You might add some logging to -updateClicked to reveal double-taps.
Another thought, while your #synchronized block is a good idea, it will only hold off other threads from executing those statements until the first thread exits the block. I suggest moving the -stopUpdatingLocation message to be the first statement inside that #synchronized block. That way, once you decide to act on some new GPS data you immediately tell CLLocationManager to stop sending new data.
Can you try and debug your application to see where the control goes when calling updateCells? Doesn't seem to be anything apparently wrong with the app.
Make sure that there are no memory warnings while you are in the LoadingViewController class. If there is a memory warning and your RootViewController's view is being released, then the viewDidLoad will be called again when you do a pop to RootViewController.
Keep breakpoints in viewDidLoad and updateCells. Are you sure you are not calling LoadingViewController anywhere else?
So, I never did get this to work. I observe this behavior on the device only every time I call popViewController programatically instead of allowing the default back button on the navigation controller to do the popping.
My workaround was to build a custom loading view, and flip the screen to that view every time there would be a delay due to accessing the internet. My method takes a boolean variable of yes or no - yes switches to the loading screen and no switches back to the normal view. Here's the code:
- (void)switchViewsToLoading:(BOOL)loading {
// Start the Animation Block
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:nil context:context];
[UIView setAnimationTransition: UIViewAnimationTransitionFlipFromLeft forView:self.tableView cache:YES];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:.75];
// Animations
if(loading) {
if (lv == nil) { lv = [[LoadingViewController alloc] initWithNibName:#"Loading" bundle:nil]; }
[self.view addSubview:lv.view];
[self.view sendSubviewToBack:self.tableView];
self.title = #"TrailBehind";
}
else {
[lv.view removeFromSuperview];
}
// Commit Animation Block
[UIView commitAnimations];
//It looks kind of dumb to animate the nav bar buttons, so set those here
if(loading) {
self.navigationItem.rightBarButtonItem = nil;
self.navigationItem.leftBarButtonItem = nil;
self.title = #"TrailBehind";
}
else {
UIBarButtonItem *feedback = [[UIBarButtonItem alloc] initWithTitle:#"Feedback" style:UIBarButtonItemStylePlain target:self action:#selector(feedbackClicked)];
self.navigationItem.rightBarButtonItem = feedback;
UIBarButtonItem *update = [[UIBarButtonItem alloc] initWithTitle:#"Move Me" style:UIBarButtonItemStylePlain target:self action:#selector(updateClicked)];
self.navigationItem.leftBarButtonItem = update;
[feedback release];
[update release];
}
}
Looking at your original code, I suspect this block very much:
- (void)viewDidLoad {
...
lv = [[LoadingViewController alloc] initWithNibName:#"Loading" bundle:nil];
[self.navigationController pushViewController:lv animated:YES];
[super viewDidLoad];
}
viewDidLoad is called every time the NIB is loaded, which can happen multiple times, especially if you run low on memory (something that seems likely given your remark that it only happens on device). I recommend that you implement -didReciveMemoryWarning, and after calling super, at the very least print a log so you can see whether it's happening to you.
The thing that bothers me about the code above is that you're almost certainly leaking lv, meaning that there may be an increasing number of LoadingViewControllers running around. You say it's a class variable. Do you really mean it's an instance variable? ivars should always use accessors (self.lv or [self lv] rather than lv). Do not directly assign to them; you will almost always do it wrong (as you are likely dong here).
I came across this while searching for the exact same issue, so while I'm sure you've already solved your problem by now, I figured I'd post my solution in case someone else runs across it...
This error seems to be caused when you assign two IBActions to the same UIButton in interface builder. It turned out that the button I used to push the view controller onto the stack was assigned to two IBActions, and each one was pushing a different controller onto the navigationController's stack (although you'll only end up seeing one of them - perhaps the last one to be called). So anyway, pressing the back button on the topmost view doesn't really dismiss it (or maybe it's dismissing the 2nd, unseen controller), and you have to press twice to get back.
Anyway, check your buttons and be sure they're only assigned to a single IBAction. That fixed it for me.