Shade
Shade

Reputation: 10001

Refresh UINavigationController?

I have a UINavigationController with two ViewControllers on the stack. At a certain point in the program execution, the second view controller is visible on the screen and at that moment, I would like to replace that ViewController with another. However, it's not working. Here is my code:

UINavigationController * thisNavController = self.waitingController;

// remove the Dummy and set the new page instead
NSMutableArray * newControllers = [NSMutableArray arrayWithArray: thisNavController.viewControllers];
[newControllers replaceObjectAtIndex: ([thisNavController.viewControllers count] - 1) withObject: page];
NSLog (@"visible before: %@", [thisNavController.visibleViewController description]);
[thisNavController setViewControllers: [NSArray arrayWithArray: newControllers] animated: YES];
NSLog (@"visible after: %@", [thisNavController.visibleViewController description]);
[thisNavController.visibleViewController.view setNeedsDisplay];

The above code produces this output:

2011-05-05 13:30:22.201 myApp[3286:207] visible before: <DummyViewController: 0x4c8b4c0>
2011-05-05 13:30:22.209 myApp[3286:207] visible after: <RealViewController: 0x60173f0>

But what is shown on the screen does not change. It seems that everything works fine after I switch tabs, so it seems that it is a redrawing problem, but setNeedsDisplay does nothing and I couldn't find a method that tells the NavigationController that its viewControllers have changed.

Is there some refresh mechanism that I have to trigger to refresh the screen?

Upvotes: 1

Views: 2729

Answers (2)

Shade
Shade

Reputation: 10001

The problem turned out to be the fact that I was trying to replace the view controller stack before the initial transition animation for the Dummy controller has finished. This can be prevented in the following manner.

First, preserve the (eventual) delegate, set the current object as the delegate, set a flag that animation is in progress and push the new controller:

self.oldNavigationControllerDelegate = self.waitingController.navigationController.delegate;
self.waitingController.navigationController.delegate = self;
self.isAnimating = YES;
[viewController.navigationController pushViewController: [[DummyViewController alloc] init] animated: YES];

Then, implement the UIViewControllerDelegate protocol methods as follows:

#pragma mark -
#pragma mark UINavigationControllerDelegate methods

- (void) navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if (navigationController == self.waitingController.navigationController)
        self.isAnimating = YES;
}

- (void) navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if (navigationController == self.waitingController.navigationController) {
        self.isAnimating = NO;
        if (self.readyPage != nil)
            [self pageIsReady: self.readyPage]; // method to load the ready controller
    }
}

After that, whenever your content/controller/download/whatever is ready, make sure that the navigation controller is no longer animating. If it is, set a flag that the page is ready. If it isn't, load the page:

if (self.isAnimating)
    self.readyPage = controller;
else
    [self pageIsReady: controller];

And, of course, implement the actual loading of the new stack (as usual):

- (void) pageIsReady: (UIViewController *) page {
    // this method should replace the dummy that is spinning there

    UINavigationController * thisNavController = self.waitingController.navigationController;

    // remove the Dummy and set the new page instead
    NSMutableArray * newControllers = [NSMutableArray arrayWithArray: thisNavController.viewControllers];
    [newControllers replaceObjectAtIndex: ([thisNavController.viewControllers count] - 1) withObject: page];
    thisNavController.viewControllers = [NSArray arrayWithArray: newControllers];
    thisNavController.delegate = self.oldNavigationControllerDelegate; // restore the original delegate

    // clean up
    self.isAnimating = NO;
    self.readyPage = nil;
    self.waitingController = nil;
    self.oldNavigationControllerDelegate = nil;
}

This makes everybody happy :P

Upvotes: 0

petert
petert

Reputation: 6692

One solution would be to say add 2 (initial) view controllers when your app is started, and only allow navigation from the 2nd and 3rd ones, falling back to the 1st (root) view controller in your senario described. You never allow navigation back to this 1st view controller or from this 1st view controller to the 2nd; you see this sort of behaviour in some of Apple's apps, like iTunes and Remote - if there's no network connect the app shows a no-network connection view immediately.

So, when you want to show the 1st view controller above, you do something like:

NSArray *array = [navigationController popToRootViewControllerAnimated:NO];

Without more info about the navigation behaviour of your app I hope this helps.

Or show a modal view controller?

Upvotes: 1

Related Questions