Adjusting position together with interactivePopGestureRecognizer - iphone

I have a custom control, containing a row of buttons, that mimics the tab bar. This control slides out of view when the UINavigationController navigates away from the root view controller, and slides back in when navigating to the root.
With iOS 7, there is the UIScreenEdgePanGestureRecognizer that gives the swipe-to-go-back gesture. I am thus modifying my custom control so that the slide amount corresponds to the UIScreenEdgePanGestureRecognizer's translation.
Problem is, when the user releases the touch, how do I tell if the UINavigationController will navigate back or bounce back to the original view?
[self.interactivePopGestureRecognizer addTarget:self action:#selector(panningBack:)];
- (void) panningBack:(UIPanGestureRecognizer *)recognizer
{
// Snipped - Code that reads the recognizer translation and adjust custom control y position
if (recognizer.state == UIGestureRecognizerStateEnded)
{
// Question: Does it go back, or does it not?
// If it goes back, slide custom control into view
// Else slide custom control out of view
}
}

I know this is a rather old question so the answer might not be of use to the OP but maybe to some one else. I faced the same problem yesterday and did a lot of searching on SO an the rest of the web without really finding anything.
So here is the solution I used for a similar problem. This is implemented in the navigationcontroller delegate but i guess you can do it some other place if that fits your need better.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
id<UIViewControllerTransitionCoordinator> tc = navigationController.topViewController.transitionCoordinator;
[tc notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
NSLog(#"DONE!!!");
NSLog(#"Container View: %#", [context containerView]);
NSLog(#"From VC: %#", [context viewControllerForKey:UITransitionContextFromViewControllerKey]);
NSLog(#"To VC: %#", [context viewControllerForKey:UITransitionContextToViewControllerKey]);
NSLog(#"Initially Interactive: %i", [context initiallyInteractive]);
NSLog(#"Completion Curve: %d", [context completionCurve]);
NSLog(#"Is Animated: %i", [context isAnimated]);
NSLog(#"Is Cancelled: %i", [context isCancelled]);
NSLog(#"Is Interactive: %i", [context isInteractive]);
NSLog(#"Percent Complete: %f", [context percentComplete]);
NSLog(#"Presentation Style: %d", [context presentationStyle]);
NSLog(#"Transition Duration: %f", [context transitionDuration]);
}];
}
This will fire when user lift her finger and the animation is rather reversed or completed. The [context isCancelled]; will tell you if it reversed or completer. And there is also a lot of other nice info in the context object that can be to use.

I would say the easiest solution is to use the default gesture implemented in the navigation controller. When the view appears show the bar and hide the bar when it disappears.
An elegant solution for knowing if it should go back or not, would be to detect the last movement.
Meaning if the user went some pixels to the left and releases -> bounce back
if the user went some pixels to the right and releases -> show previous controller
This can be done if you save the location in state changed and compare it to state ended.
Getting the point like this:
CGPoint point = [recognizer locationInView:view];

Related

how to display UIActivityIndicatorView BEFORE rotation begins

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.

Screen corruption after dismissing modal UIImagePickerController subclass

I am having trouble dismissing a modal view controller containing a UIImagePickerController (ZBarScannerController). For some reason, after dismissing the controller after scanning a bar code with the iphone camera, the view controller always leaves a rectangle of stale graphical data over the same area as the ZBarScannerController's tool bar. The corrupt data is always a portion of whatever image the camera was seeing at the moment.
An image of the problem (corrupt area in red rectangle):
It is impossible to remove that rectangle of corrupt screen data except by backgrounding / killing the app. Also, if I specify NO when I dismiss the modal picker, the OS will remove the view controller while still displaying the controller on screen, causing crashes if I interact with any controls on the modal view. How can I go about fixing the problem?
Code for dismissing the controller:
- (void) imagePickerController: (UIImagePickerController*) reader
didFinishPickingMediaWithInfo: (NSDictionary*) info
{
id<NSFastEnumeration> results =
[info objectForKey:ZBarReaderControllerResults];
for (ZBarSymbol *oSymbol in results) {
//process result
//Ensure that QR code is decoded
zbar_symbol_type_t type = oSymbol.type;
if (type == ZBAR_QRCODE) {
//Get Barcode Data
NSString *dataStr = oSymbol.data;
[self processCommand:dataStr];
}
}
//[reader dismissModalViewControllerAnimated:NO];
[[m_oReaderController parentViewController] dismissModalViewControllerAnimated:YES];
}
Code for making the controller:
- (void)onQRCameraActivate:(id)sender {
IPOProofAppDelegate *oAppDelegate = (IPOProofAppDelegate *) [[UIApplication sharedApplication] delegate];
if (m_oReaderController == nil) {
m_oReaderController = [[ZBarReaderViewController alloc] init];
m_oReaderController.readerDelegate = self;
ZBarImageScanner *oScanner = m_oReaderController.scanner;
[oScanner setSymbology:0 config:ZBAR_CFG_ENABLE to:0];
[oScanner setSymbology:ZBAR_QRCODE config:ZBAR_CFG_ENABLE to:1];
}
[self.navigationController presentModalViewController:m_oReaderController animated:YES];
}

iOS: AutoRotating between NIBs

My universal app is a single full screen view. Pressing a button flips to reveal a settings page:
- (void) showSettings
{
FlipsideViewController * flipsideVC = [FlipsideViewController alloc];
NSString * settingsNib;
if ( isIPad() )
settingsNib = isCurrentlyPortrait() ? #"settings_iPad_portrait" : #"settings_iPad_landscape";
else
settingsNib = #"settings_iPhone";
[flipsideVC initWithNibName: settingsNib
bundle: nil ];
flipsideVC.delegatePointingToMainVC = self;
flipsideVC.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController: flipsideVC
animated: YES ];
[flipsideVC release];
}
and the settings page invokes the delegate method: I recreate the main view in light of the changed settings, and flip back.
- (void) settingsDidQuit:(FlipsideViewController *) flipsideVC
{
[self createOrRecreateWheelView];
[self dismissModalViewControllerAnimated: YES];
}
But what if the user rotates the iPad on the settings page? Apple decrees that my app must handle this. But how to do this? can I dynamically load a new XIB for the settings page?
I can't see a way to do that, so my attempted solution is to catch the rotation within the settings view, ...
- (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation) oldInterfaceOrientation
{
[self.delegatePointingToMainVC settingsOrientationChanged];
}
...and call back to the main view controller, which dissolves the settings view controller and recreates it in light of the current orientation.
- (void) settingsOrientationChanged
{
[self dismissModalViewControllerAnimated: YES];
[self showSettings];
}
There is a trivial problem straight away -- didRotateFromInterfaceOrientation gets triggered automatically when the settings page loads. I can prevent this by setting a boolean to false in init, and modifying thus:
- (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation) oldInterfaceOrientation
{
if (initialized)
[self.delegatePointingToMainVC settingsOrientationChanged];
initialized = true;
}
problem with this approach is that I navigate to the settings page, rotate the device, and it momentarily shows the correct settings page, before flicking back to my main view.
I think there is a threading problem here. But maybe my whole approach is wrong. Can somebody suggest a better solution?
I'm not sure I understand the problem. You want the settings view (loaded from NIB) to autorotate? You should just return YES for the orientation you want the autorotation to be performed in the shouldAutorotateToInterfaceOrientation: and set the autoresizing mask of the views inside the XIB accordingly to your needs.
There's no need to call back the main view controller and tell him to push a new settings view controller. The rotation behavior of the views is determined by the autoresizing mask properties of each view and the implementation of shouldAutorotateToInterfaceOrientation: method of the associated view controller and just that. If want to do more advanced animations, though, you can set up and manage them in the willRotateToInterfaceOrientation:duration: and didRotateFromInterfaceOrientation: methods.

iPhone: Get a list of clickable areas on screen in cocoa touch

I have an iPhone app and I want to get a list of co-ords that are clickable by a user. I want to automate testing and have a client app click around on screen but just choosing random coords isn't ideal so a list of coords that are definitely clickable would be much better.
So far I have this the view passed in is top level window:
getSubViewsCoords:(UIView *)view {
iAppDelegate* appDelegate = [[UIApplication sharedApplication] delegate];
for (UIView *tempView in [view subviews]) {
// check if view responds to touch i.e. is an interactive view.
// go up through responder chain
// check if it reacts
// if it does then add to the gorilla's list
UIView *responderObj = tempView;
while (responderObj = [responderObj nextResponder]) {
if([responderObj respondsToSelector:#selector(touchesBegan:withEvent:)]){
// get xy etc and add to instance var
//then recall this function
// convert to top level windows co-ordinate system.
CGRect viewRect = [tempView convertRect:[tempView bounds] toView:nil];
clickableAreas = [clickableAreas stringByAppendingString: NSStringFromCGRect(viewRect)];
clickableAreas = [clickableAreas stringByAppendingString: #"\n"];
break;
}
}
[self getSubViewsCoords:tempView];
Is this the correct way to go about it? Is there an easier way to get this information?
Thanks in advance for your help.
If you haven't already read it, you'll probably find this article helpful, Automated user interface testing on the iPhone.

Why does popViewController only work every other time

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.