使用哪种方法替换视图控制器(或从导航堆栈中删除),而不是使用推送转场?

71

我有一个小型iPhone应用程序,它使用导航控制器显示3个视图(这里全屏):

Xcode screenshot

首先它会显示一个社交网络列表(Facebook、Google+等):

list screenshot

然后它会显示一个OAuth对话框,要求输入凭证:

login screenshot

然后(在同一个UIWebView中)需要获取权限:

permissions screenshot

最后,它会显示包含用户详细信息的最后一个视图控制器(在真实应用程序中,这将是菜单,可以从中开始多人游戏):

details screenshot

这一切都很好,但是我有一个问题,当用户想要返回并选择另一个社交网络时:
用户点击返回按钮,但没有显示第一个视图,而是显示第二个视图,再次请求OAuth凭证/权限。
我该怎么办?Xcode 5.0.2提供了非常有限的segue选项 - 推送(push)、模态(modal)(我不能使用它,因为它会遮挡我的游戏所需的导航栏)和自定义(custom)。
我是iOS编程新手,但之前我开发过Adobe AIR移动应用程序,在那里可以1)替换视图而不是推送和2)从导航堆栈中删除不需要的视图。
请问如何在本机应用程序中实现相同的功能?

在这里查看:https://dev59.com/iWoy5IYBdhLWcg3wUcf_#67107414 - Fattie
14个回答

59

在上面讨论的各种过渡效果中,这是我的解决方案。它具有以下优点:

  • 可以在视图堆栈的任何位置工作,而不仅仅是顶部视图(不确定是否实际上需要或甚至是否可能触发,但是它在这里)。
  • 在显示替换内容之前,它不会导致弹出先前的视图控制器或转场效果,只是展示一个自然的过渡效果,返回导航与源控制器相同的后退导航。

Segue 代码:

- (void)perform {
    // Grab Variables for readability
    UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
    UIViewController *destinationController = (UIViewController*)[self destinationViewController];
    UINavigationController *navigationController = sourceViewController.navigationController;

    // Get a changeable copy of the stack
    NSMutableArray *controllerStack = [NSMutableArray arrayWithArray:navigationController.viewControllers];
    // Replace the source controller with the destination controller, wherever the source may be
    [controllerStack replaceObjectAtIndex:[controllerStack indexOfObject:sourceViewController] withObject:destinationController];

    // Assign the updated stack with animation
    [navigationController setViewControllers:controllerStack animated:YES];
}

不错!如果你正在处理“上一个”而不是“下一个”的情况,那么怎么样加个弹出过渡效果(或将所有内容向右滑动)? - finneycanhelp
我不确定我真正理解你的问题,但是你可以修改堆栈以插入/替换前一个视图为任何你喜欢的内容,然后简单地关闭顶部视图以触发正常的转换... - ima747

39

您可以使用自定义转场:为此,您需要创建一个继承UIStoryboardSegue的类(例如MyCustomSegue),然后可以使用以下内容覆盖“perform”

-(void)perform {
    UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
    UIViewController *destinationController = (UIViewController*)[self destinationViewController];
    UINavigationController *navigationController = sourceViewController.navigationController;
    // Pop to root view controller (not animated) before pushing
    [navigationController popToRootViewControllerAnimated:NO];
    [navigationController pushViewController:destinationController animated:YES];    
}

此时进入Interface Builder,选择“自定义”segue,并输入你的类名(例如MyCustomSegue)

In swift

override func perform() {
    if let navigationController = self.source.navigationController {
        navigationController.setViewControllers([self.destination], animated: true)
    }
}

谢谢,但您确定可以使用自定义segue实现替换第二个场景为第三个场景(而不是推送),以便触摸“返回”按钮会将用户返回到第一个场景吗?我正在阅读https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/Reference/Reference.html,但我无法弄清楚如何做到这一点。 - Alexander Farber
1
在您的第一条建议中,您没有将 sourceViewController.navigationController 保存到变量中,因此在 perform 中的最后一个命令之前变成了 nil - 这就是为什么它对我不起作用的原因。但是当前版本完美地工作,谢谢!Lauro和Bhavya的答案也非常有深度。 - Alexander Farber
@Daniele。你从哪里得到 destinationViewController 的引用的? - zulkarnain shah
虽然它能够工作,但是由于动画到目的地是从rootViewController动画出来的,因此故事流程被打断,看起来很奇怪。 - Teddy

27

自定义转场对我没有起作用,因为我有一个启动页视图控制器,并且想要替换它。由于列表中只有一个视图控制器,所以popToRootViewController仍然将启动页保留在堆栈中。 我使用以下代码替换了单个控制器

-(void)perform {
    UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
    UIViewController *destinationController = (UIViewController*)[self destinationViewController];
    UINavigationController *navigationController = sourceViewController.navigationController;
    [navigationController setViewControllers:@[destinationController] animated:YES];
}

现在在 Swift 4 中:

class ReplaceSegue: UIStoryboardSegue {

    override func perform() {
        source.navigationController?.setViewControllers([destination], animated: true)
    }
}

现在在Swift 2.0中

class ReplaceSegue: UIStoryboardSegue {

    override func perform() {
        sourceViewController.navigationController?.setViewControllers([destinationViewController], animated: true)
    }
}

我有一个汉堡菜单作为navigationViewController。你的代码可以运行,但是它替换了我在菜单中使用的tableView而不是viewController(根视图控制器)。 - Paul Bénéteau
有趣的 @greenpoisononeTV,你在哪个控制器上调用了“performSegue”?因为源视图控制器和目标控制器都是使用segue设置的。 - christophercotton
我使用ctrl+拖动从我的tableViewCell创建了一个segue到一个viewController。该segue被设置为自定义,使用ReplaceSegue类。 - Paul Bénéteau

21

对于这个问题,我认为答案很简单,只需:

  1. 从NavigationController获取视图控制器数组
  2. 删除最后一个视图控制器(当前视图控制器)
  3. 将新的视图控制器插入到最后
  4. 然后像下面一样将ViewControllers数组设回navigationController中:

     if let navController = self.navigationController {
        let newVC = DestinationViewController(nibName: "DestinationViewController", bundle: nil)
    
        var stack = navController.viewControllers
        stack.remove(at: stack.count - 1)       // remove current VC
        stack.insert(newVC, at: stack.count) // add the new one
        navController.setViewControllers(stack, animated: true) // boom!
     }
    

可以完美地与Swift 3一起使用。

希望能对一些新手有所帮助。

干杯。


目标视图控制器是什么? - Paul Bénéteau
这是您要导航到的视图控制器。 - Rameswar Prasad
无法使用该代码加载我的目标VC。崩溃了。您将此代码添加到哪个方法中? - zulkarnain shah

18

ima747的swift 2版本答案:

override func perform() {
    let navigationController: UINavigationController = sourceViewController.navigationController!;

    var controllerStack = navigationController.viewControllers;
    let index = controllerStack.indexOf(sourceViewController);
    controllerStack[index!] = destinationViewController

    navigationController.setViewControllers(controllerStack, animated: true);
}

正如他所提到的,它具有以下优点:

  • 可以在视图堆栈中的任何位置工作,而不仅仅是顶部视图(不确定是否真正需要或者技术上是否可能触发,但是它在其中)。
  • 在显示替换之前,它不会导致弹出或过渡到上一个视图控制器,而只会使用自然的转换显示新控制器,并且返回导航与源控制器的相同后退导航。

6
为了让它更安全,你可以在if let条件语句中获取索引,这样就不需要强制解包变量,从而避免可能出现的错误。 - Afonso
那么,使用这种实现方式,sourceViewController 什么时候从堆栈中弹出? - zulkarnain shah
你是从哪里获取到 destinationViewController 的引用的? - zulkarnain shah

9
使用“取消转场segue”可能是解决这个问题最合适的方法。我同意Lauro的观点。以下是从detailsViewController [或viewController3] 设置取消segue到myAuthViewController [或viewController1]的简要说明。这基本上就是如何通过代码执行取消segue的方式。
  • Implement an IBAction method in the viewController you want to unwind to(in this case viewController1). The method name can be anything so long that it takes one argument of the type UIStoryboardSegue.

    @IBAction func unwindToMyAuth(segue: UIStoryboardSegue) {
    println("segue with ID: %@", segue.Identifier)
    }
    
  • Link this method in the viewController(3) you want to unwind from. To link, right click(double finger tap) on the exit icon at the top of the viewController, at this point 'unwindToMyAuth' method will show in the pop up box. Control click from this method to the first icon, the viewController icon(also present at the top of the viewController, in the same row as the exit icon). Select the 'manual' option that pops up.

  • In the Document outline, for the same view(viewController3), select the unwind segue you just created. Go to the attributed inspector and assign a unique identifier for this unwind segue. We now have a generic unwind segue ready to be used.

  • Now, the unwind segue can be performed just like any other segue from the code.

    performSegueWithIdentifier("unwind.to.myauth", sender: nil)
    

这种方法可以让你从viewController3返回到viewController1,而无需将viewController2从导航层次结构中移除。

与其他segue不同,反向segue不会实例化一个视图控制器,它只会去到导航层次结构中现有的一个视图控制器。


5

如之前的回答所述,将弹出窗口从未动画变为推送动画再次变为弹出动画会显得不太好看,因为用户会看到实际的过程。我建议您先进行推送动画,然后再移除之前的视图控制器。操作方式如下:

extension UINavigationController {
    func replaceCurrentViewController(with viewController: UIViewController, animated: Bool) {
        pushViewController(viewController, animated: animated)
        let indexToRemove = viewControllers.count - 2
        if indexToRemove >= 0 {
            viewControllers.remove(at: indexToRemove)
        }
    }
}

1
谢谢你的回答,它非常有效。我喜欢扩展的想法。 - Murat Akdeniz

5
这里有一个快速的Swift 4/5解决方案,创建一个自定义连线(seque),用新的视图控制器替换(顶部栈)旧的视图控制器(不带动画):
class SegueNavigationReplaceTop: UIStoryboardSegue {

    override func perform () {
        guard let navigationController = source.navigationController else { return }
        navigationController.popViewController(animated: false)
        navigationController.pushViewController(destination, animated: false)
    }
}

2
swift3 中创建一个segue - 添加标识符 - 在segue(storyboard)中添加并设置自定义storyboard类,来自cocoatouch文件 - 在自定义类中重写perform()方法
override func perform() {
    let sourceViewController = self.source
    let destinationController = self.destination
    let navigationController = sourceViewController.navigationController
    // Pop to root view controller (not animated) before pushing
    if self.identifier == "your identifier"{
    navigationController?.popViewController(animated: false)
    navigationController?.pushViewController(destinationController, animated: true)
    }
    }

- 您还需要在源视图控制器中覆盖一个方法。
  override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
    return false
}

2

请使用以下代码作为最后一个视图控制器 您可以使用其他按钮或将其放置在您自己的位置,而不是我使用的取消按钮

- (void)viewDidLoad
{
 [super viewDidLoad];

 [self.navigationController setNavigationBarHidden:YES];

 UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(dismiss:)];

self.navigationItemSetting.leftBarButtonItem = cancelButton;

}

- (IBAction)dismissSettings:(id)sender
{

// your logout code for social media selected
[self.navigationController popToRootViewControllerAnimated:YES];

}

你可能是想说 setNavigationBarHidden:NO 吧?你的代码很好用,谢谢。这也是一个不错的位置来放置社交网络注销代码。 - Alexander Farber

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