如何在一个操作中从UINavigationController中弹出一个视图并替换为另一个视图?

84

我有一个应用程序,需要从UINavigationController的堆栈中删除一个视图,并使用另一个视图替换它。情况是第一个视图创建一个可编辑项,然后用该项的编辑器替换自己。当我在第一个视图中尝试最明显的解决方案时:

MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

[self retain];
[self.navigationController popViewControllerAnimated: NO];
[self.navigationController pushViewController: mevc animated: YES];
[self release];

我的行为非常奇怪。通常编辑器视图会出现,但是如果我尝试使用导航栏上的返回按钮,会出现额外的屏幕,一些是空白的,一些只是混乱的。标题也变得随机了。就像导航栈完全被破坏了一样。

对于这个问题,有更好的解决方法吗?

谢谢, Matt

16个回答

137

我发现你完全不需要手动操作viewControllers属性。基本上有两件事情比较棘手:

  1. self.navigationController 如果self当前不在导航控制器的堆栈中,则会返回nil,因此在失去访问权限之前将其保存到局部变量中。
  2. 您必须对self进行retain(和适当的release)或拥有您所处方法的对象将被释放,从而引起奇怪的行为。

完成这些准备工作后,就像正常情况下弹出和推入一样。 这段代码将立即用另一个控制器替换顶部控制器。

// locally store the navigation controller since
// self.navigationController will be nil once we are popped
UINavigationController *navController = self.navigationController;

// retain ourselves so that the controller will still exist once it's popped off
[[self retain] autorelease];

// Pop this controller and replace with another
[navController popViewControllerAnimated:NO];
[navController pushViewController:someViewController animated:NO];

如果您将最后一行的animated更改为YES,则新屏幕将实际进行动画,同时弹出的控制器也会进行动画。看起来很不错!


太棒了!更好的解决方案。 - emmby
太棒了。虽然我不需要调用[[self retain] autorelease],但它仍然可以正常工作。 - iamj4de
4
或许很明显,但是你可以把上面的代码放在一个动画块中,以实现过渡的动画效果:[UIView beginAnimations:@"View Flip" context:nil]; [UIView setAnimationDuration:0.80]; [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut]; [UIView setAnimationTransition: UIViewAnimationTransitionFlipFromRight forView:navController.view cache:NO]; [navController pushViewController:newController animated:YES]; [UIView commitAnimations]; - Martin
11
仅通过删除retain/autorelease行,就可以与ARC完美配合。 - Ian Terrell
2
@TomerPeled 是的,这个答案已经快5年了...我想那是在iOS 3时代的情况。API已经发生了足够的变化,我不确定这是否仍然是最好的答案。 - Alex Wayne
显示剩余5条评论

56

以下方法对我来说更加舒适,并且还适用于ARC:

UIViewController *newVC = [[UIViewController alloc] init];
// Replace the current view controller
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[[self navigationController] viewControllers]];
[viewControllers removeLastObject];
[viewControllers addObject:newVC];
[[self navigationController] setViewControllers:viewControllers animated:YES];

1
@LukeRogers,这对我造成了以下警告:在意料之外的状态下完成导航转换。导航栏子视图树可能会损坏。有什么方法可以抑制它吗? - zaitsman
使用此解决方案,您可以覆盖弹出窗口。 要在DetailView中显示,您的代码应该如下所示: if(indexPath.row == 0){UIViewController *newVC = [[UIViewController alloc] init];newVC = [self.storyboard instantiateViewControllerWithIdentifier:@"Item1VC"]; NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[_detailViewController.navigationController viewControllers]]; [viewControllers removeLastObject];[viewControllers addObject:newVC]; [_detailViewController.navigationController setViewControllers:viewControllers animated:YES];} - LAOMUSIC ARTS
我正在寻找的东西。 - JERC

9

从经验来看,您需要直接操作UINavigationController的viewControllers属性。以下内容应该可以正常工作:

MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

[[self retain] autorelease];
NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];
[controllers removeLastObject];
self.navigationController.viewControllers = controllers;
[self.navigationController pushViewController:mevc animated: YES];

注意:我将保留/释放更改为保留/自动释放,因为这通常更加健壮 - 如果在保留/释放之间发生异常,你会泄漏self,但是自动释放会解决这个问题。

7
经过许多努力(并调整了Kevin的代码),我终于找到了在被弹出堆栈的视图控制器中执行此操作的方法。我遇到的问题是当我从控制器数组中删除最后一个对象后,self.navigationController返回空值。我认为这是UIViewController文档中关于navigationController实例方法的这行文字导致的:"Only returns a navigation controller if the view controller is in its stack." 我认为一旦当前视图控制器从堆栈中移除,其navigationController方法将返回nil。
以下是已经调整好的代码:
UINavigationController *navController = self.navigationController;
MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];
[controllers removeLastObject];
navController.viewControllers = controllers;
[navController pushViewController:mevc animated: YES];

这让我感到非常困惑! - LAOMUSIC ARTS

4
谢谢,这正是我需要的。我还将其放入动画中以获得翻页效果:
        MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

    UINavigationController *navController = self.navigationController;      
    [[self retain] autorelease];

    [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration: 0.7];
    [UIView setAnimationTransition:<#UIViewAnimationTransitionCurlDown#> forView:navController.view cache:NO];

    [navController popViewControllerAnimated:NO];
    [navController pushViewController:mevc animated:NO];

    [UIView commitAnimations];

0.6的持续时间很快,适合3GS及更高版本,0.8对于3G来说仍然有点太快了。

Johan


你的代码和我用的一模一样,太棒了!谢谢。有一个注意点:使用翻页过渡效果时,在视图底部会出现白色瑕疵(谁知道为什么),但是使用翻转效果就可以正常工作。总之,这是非常好的、紧凑的代码! - David H

3

如果你想通过popToRootViewController展示任何其他的视图控制器,那么你需要按照以下步骤操作:

         UIViewController *newVC = [[WelcomeScreenVC alloc] initWithNibName:@"WelcomeScreenVC" bundle:[NSBundle mainBundle]];
            NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[[self navigationController] viewControllers]];
            [viewControllers removeAllObjects];
            [viewControllers addObject:newVC];
            [[self navigationController] setViewControllers:viewControllers animated:NO];

现在,您之前的堆栈将被删除,并创建一个新的堆栈,其中包含您所需的rootViewController。

1
最近我也需要做类似的事情,基于Michael的答案,我找到了解决方案。在我的情况下,我需要从导航栈中移除两个视图控制器,然后添加一个新的视图控制器。在我的情况下,调用
[controllers removeLastObject];
两次就可以了。

UINavigationController *navController = self.navigationController;

// retain ourselves so that the controller will still exist once it's popped off
[[self retain] autorelease];

searchViewController = [[SearchViewController alloc] init];    
NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];

[controllers removeLastObject];
// In my case I want to go up two, then push one..
[controllers removeLastObject];
navController.viewControllers = controllers;

NSLog(@"controllers: %@",controllers);
controllers = nil;

[navController pushViewController:searchViewController animated: NO];


1
NSMutableArray *controllers = [self.navigationController.viewControllers mutableCopy];
    for(int i=0;i<controllers.count;i++){
       [controllers removeLastObject];
    }
 self.navigationController.viewControllers = controllers;

这在控制台中为我引起了一个警告 - 在意料之外的状态下完成导航转换。导航栏子视图树可能会损坏。有什么方法可以抑制它吗? - zaitsman

1

这个UINavigationController实例方法可能有效...

一直弹出视图控制器,直到指定的视图控制器成为顶部视图控制器,然后更新显示。

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated

1
我最喜欢的方法是使用UINavigationController上的一个类别。以下应该可以正常工作:
UINavigationController+Helpers.h #import
@interface UINavigationController (Helpers)

- (UIViewController*) replaceTopViewControllerWithViewController: (UIViewController*) controller;

@end

UINavigationController+Helpers.m
#import "UINavigationController+Helpers.h"

@implementation UINavigationController (Helpers)

- (UIViewController*) replaceTopViewControllerWithViewController: (UIViewController*) controller {
    UIViewController* topController = self.viewControllers.lastObject;
    [[topController retain] autorelease];
    UIViewController* poppedViewController = [self popViewControllerAnimated:NO];
    [self pushViewController:controller animated:NO];
    return poppedViewController;
}

@end

然后从您的视图控制器中,您可以通过以下方式将顶部视图替换为新视图:

[self.navigationController replaceTopViewControllerWithViewController: newController];

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接