模态视图控制器 - 如何显示和取消显示

82

在过去的一周里,我一直在努力解决显示和消除多个视图控制器的问题。我创建了一个示例项目,并直接从该项目中粘贴了代码。我有3个视图控制器及其对应的.xib文件。MainViewController、VC1和VC2。主视图控制器上有两个按钮。

- (IBAction)VC1Pressed:(UIButton *)sender
{
    VC1 *vc1 = [[VC1 alloc] initWithNibName:@"VC1" bundle:nil];
    [vc1 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc1 animated:YES completion:nil];
}

这个操作可以顺利打开VC1。在VC1中,我有另一个按钮,应该在同时关闭VC1的情况下打开VC2。

- (IBAction)buttonPressedFromVC1:(UIButton *)sender
{
    VC2 *vc2 = [[VC2 alloc] initWithNibName:@"VC2" bundle:nil];
    [vc2 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc2 animated:YES completion:nil];
    [self dismissViewControllerAnimated:YES completion:nil];
} // This shows a warning: Attempt to dismiss from view controller <VC1: 0x715e460> while a presentation or dismiss is in progress!


- (IBAction)buttonPressedFromVC2:(UIButton *)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
} // This is going back to VC1. 

我希望在返回主视图控制器的同时,VC1应该被永久性地从内存中删除。只有当我在主控制器上点击VC1按钮时才会显示VC1。

在主视图控制器上的另一个按钮也应该能够直接显示VC2,跳过VC1,并且当在VC2上点击按钮时返回到主控制器。没有长时间运行的代码、循环或任何计时器。只是对视图控制器进行了基本的调用。

6个回答

190

这行代码:

[self dismissViewControllerAnimated:YES completion:nil];

在这种情况下,发送的信息并不是发给自身,而是发给呈现视图控制器,请求它执行关闭操作。当你呈现一个视图控制器时,你会创建一个呈现 VC 和被呈现 VC 之间的关系。因此,在呈现时不应该销毁呈现 VC(被呈现的 VC 无法将关闭消息发送回来...)。如果你没有意识到这点,就会让应用程序处于混乱状态。请参阅我的答案 Dismissing a Presented View Controller ,其中我建议使用以下方法进行更清晰地编写:

[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];

在您的情况下,您需要确保所有控制都在mainVC中完成。您应该使用代理将正确的消息从ViewController1发送回MainViewController,以便mainVC可以关闭VC1,然后呈现VC2。

在您的.h文件中,在@interface上方为VC1添加一个协议:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

在同一文件的@interface部分下面声明一个属性来保存委托指针:

@property (nonatomic,weak) id <ViewController1Protocol> delegate;

在 VC1 的 .m 文件中,dismiss 按钮的方法应该调用代理方法。

- (IBAction)buttonPressedFromVC1:(UIButton *)sender {
    [self.delegate dissmissAndPresentVC2]
}

在 mainVC 中创建 VC1 时,将 mainVC 设置为 VC1 的代理:

- (IBAction)present1:(id)sender {
    ViewController1* vc = [[ViewController1 alloc] initWithNibName:@"ViewController1" bundle:nil];
    vc.delegate = self;
    [self present:vc];
}

并实现委托方法:

- (void)dismissAndPresent2 {
    [self dismissViewControllerAnimated:NO completion:^{        [self present2:nil];
    }];
}

present2:可以与你的VC2Pressed:按钮IBAction方法相同。请注意,它是从完成块调用的,以确保在完全解除VC1之前不会呈现VC2。

您现在正在从VC1-> VCMain-> VC2移动,因此您可能只想要其中一个过渡进行动画处理。

在您的评论中,您对实现看似简单的事情所需的复杂性感到惊讶。我向您保证,这种委托模式在Objective-C和Cocoa的许多方面都是如此核心,而且这个例子是你可以得到的最简单的例子,你真的应该努力学习并熟悉它。

在苹果的视图控制器编程指南中,他们有这样说:

取消显示已呈现视图控制器

当需要取消显示呈现的视图控制器时,首选的方法是让呈现视图控制器取消显示。换句话说,尽可能让呈现视图控制器的相同视图控制器也负责取消显示。虽然有几种技术可以通知呈现视图控制器其已呈现的视图控制器应该被取消显示,但首选技术是委托。有关更多信息,请参见“使用委托与其他控制器通信”。

如果您真正思考您想要实现的内容以及您正在进行的方式,您将意识到向MainViewController发送消息以完成所有工作是唯一的出路,因为您不想使用NavigationController。如果您使用了NavController,则实际上是在“委派”,即使没有明确指定,也要求navController执行所有工作。需要有一个对象来保持对VC导航情况的中央跟踪,无论您做什么,都需要某种通信方法。

实际上,苹果的建议有点极端... 在正常情况下,您不需要制作专用代理和方法,您可以依赖于[self presentingViewController]dismissViewControllerAnimated:-当您在像您这样的情况下希望解雇远程对象时,您需要小心处理。

这里有一些您可以设想而不涉及所有委托麻烦的工作...

- (IBAction)dismiss:(id)sender {
    [[self presentingViewController] dismissViewControllerAnimated:YES 
                                                        completion:^{
        [self.presentingViewController performSelector:@selector(presentVC2:) 
                                            withObject:nil];
    }];

}

在请求当前控制器将我们dismiss后,我们有一个完成块,在其中调用presentingViewController中的方法以调用VC2。不需要委托。(块的一个重要卖点是它们减少了在这些情况下需要委托的需求)。然而在这种情况下,有一些障碍...

  • 在VC1中,您不知道mainVC是否实现了present2方法 - 您可能会遇到难以调试的错误或崩溃。使用委托可以帮助您避免这种情况。
  • 一旦VC1被dismiss,它就不再存在来执行完成块...还是存在?您不知道(我也不知道)...但是使用委托,您就不必担心这个问题。
  • 当我尝试运行此方法时,它只是无声地挂起,没有警告或错误。

所以...请花时间学习委托!

update2

在您的评论中,您已成功通过在VC2的dismiss按钮处理程序中使用此方法使其工作:

 [self.view.window.rootViewController dismissViewControllerAnimated:YES completion:nil]; 

这种方法确实更加简单,但它会带来许多问题。

紧密耦合
您正在将 viewController 结构硬编码在一起。例如,如果您要在 mainVC 之前插入一个新的 viewController,则必需的行为将会失效(您将导航到上一个 viewController)。在 VC1 中,您还必须 #import VC2。因此,您有相当多的相互依赖关系,这打破了 OOP/MVC 的目标。

使用代理,VC1 和 VC2 都不需要知道任何关于 mainVC 或其祖先的信息,因此我们保持所有内容松耦合和模块化。

内存
VC1 没有消失,您仍然持有指向它的两个指针:

  • mainVC 的 presentedViewController 属性
  • VC2 的 presentingViewController 属性

您可以通过日志记录来测试此方法,也可以直接从 VC2 中执行此操作。

[self dismissViewControllerAnimated:YES completion:nil]; 

它仍然可以工作,仍然可以将您带回VC1。

对我来说,这似乎是内存泄漏。

这里的警告信息提示了这一点:

[self presentViewController:vc2 animated:YES completion:nil];
[self dismissViewControllerAnimated:YES completion:nil];
 // Attempt to dismiss from view controller <VC1: 0x715e460>
 // while a presentation or dismiss is in progress!

你试图忽略VC1,而VC2是被呈现的VC,这个逻辑是有问题的。第二条信息并没有真正执行 - 或许会发生一些事情,但你仍然会得到两个指向你认为已经摆脱的对象的指针。(编辑 - 我检查过了,情况并不那么糟糕,当你回到mainVC时两个对象都会消失)

换句话说,请使用代理(delegates)。如果有帮助的话,我在这里提供了该模式的简要描述:
Is passing a controller in a construtor always a bad practice?

更新3
如果你真的想避免使用代理,这可能是最好的方法:

在VC1中:

[self presentViewController:VC2
                   animated:YES
                 completion:nil];

不过,不要轻易排除任何事情……因为我们已经确定,那根本就不会发生。

在VC2中:

[self.presentingViewController.presentingViewController 
    dismissViewControllerAnimated:YES
                       completion:nil];

如我们所知,我们并没有解除 VC1,因此我们可以通过 VC1 返回到 MainVC。MainVC 解除 VC1。由于 VC1 已经去了,那么与其一起存在的 VC2 也被呈现出来,因此您回到了干净状态的 MainVC。

虽然这仍然高度耦合,因为 VC1 需要知道 VC2 的情况,而 VC2 需要知道它是通过 MainVC-> VC1 到达的,但这是您在没有显式委托的情况下能够得到的最好结果。


1
看起来很复杂。我试图完全按照指示操作,但在中途迷失了方向。有没有其他方法可以实现这个目标?我还想补充一下,在应用程序委托中,主控制器被设置为根视图控制器。我不想使用导航控制器,但想知道为什么这样做会变得如此复杂。总结一下,当应用程序启动时,我会显示一个带有2个按钮的主视图控制器。单击第一个按钮会加载VC1。在VC1上有一个按钮,单击它应该无错误或警告地加载VC2,同时从内存中解除VC1。 - Hema
感谢您的输入。我理解现在存在两个指针指向VC1。现在,一旦我回到主视图控制器并单击VC1按钮再次进入VC1,现有的指向VC1的指针会发生什么?它会使用来自mainVC的现有引用进行更新,还是现在将其添加到3个指针中,而我不希望这样。 - Hema
解释非常详细,而且请更加关注委托。 - haxpor
@Honey - 请查看我链接的另一个答案https://dev59.com/GGUq5IYBdhLWcg3wQ-bk#14637067 ... 它最初会指向自己,但总是会被转发。这通常不被重视,所以我试图更明确地表达。 - foundry
1
@Honey - 也许是这样,但这个陈述是对一段“想象中”的伪代码的修辞回答。我想要表达的重点不是关于保留循环陷阱的问题,而是教育提问者为什么委托是一种有价值的设计模式(顺便避免了那个问题)。我认为这里的误导争议在于,问题是关于模态VCs的,但答案的价值大多在于它对委托模式的解释,利用问题和OP明显的挫败感作为催化剂。感谢您的关注(和您的编辑)! - foundry
显示剩余15条评论

13

以下是使用委托设计模式展示视图控制器版本,基于Swift语言的示例代码,结合上述铸造厂的解释和苹果公司的文档:

  1. 根据苹果公司的文档和上述铸造厂的解释(纠正了一些错误),使用委托设计模式展示视图控制器的版本:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func dismissViewController1AndPresentViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        vc1.modalTransitionStyle = UIModalTransitionStyle.FlipHorizontal
        self.presentViewController(vc1, animated: true, completion: nil)
    }

    func dismissViewController1AndPresentViewController2() {
        self.dismissViewControllerAnimated(false, completion: { () -> Void in
            let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
            self.presentViewController(vc2, animated: true, completion: nil)
        })
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.dismissViewController1AndPresentViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}
  1. 根据铸造厂的解释(纠正一些错误),使用委托设计模式实现了pushViewController版本:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func popViewController1AndPushViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        self.navigationController?.pushViewController(vc1, animated: true)
    }

    func popViewController1AndPushViewController2() {
        self.navigationController?.popViewControllerAnimated(false)
        let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
        self.navigationController?.pushViewController(vc2, animated: true)
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.popViewController1AndPushViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}

在你的例子中,ViewController 类是 mainVC,对吗? - mfaani

10

我认为你对iOS模态视图控制器的一些核心概念有所误解。当你dismiss VC1时,由VC1呈现的任何视图控制器也会被dismiss。苹果旨在使模态视图控制器以堆叠方式流动 - 在你的情况下,VC2是由VC1呈现的。你在从VC1呈现VC2后立即dismiss VC1,这样就会出现一片混乱。

要实现你想要的效果,buttonPressedFromVC1应该让mainVC在VC1自我dismiss之后立即呈现VC2。我认为这可以在不使用委托的情况下实现。代码示例如下:

UIViewController presentingVC = [self presentingViewController];
[self dismissViewControllerAnimated:YES completion:
 ^{
    [presentingVC presentViewController:vc2 animated:YES completion:nil];
 }];

请注意,self.presentingViewController被存储在另一个变量中,在vc1解除自身后,您不应该对它进行任何引用。


1
太简单了!我希望其他人能够滚动到你的答案,而不是停留在顶部的帖子。 - Ryan Loggerythm
在 OP 的代码中,为什么 [self dismiss...] 不是在 [self present...] 完成后发生的呢?并不是有异步事件发生。 - mfaani
1
@Honey,实际上,在调用presentViewController时会发生一些异步操作 - 这就是为什么它有一个完成处理程序的原因。但即使使用它,如果在呈现视图控制器后关闭呈现视图控制器,那么它呈现的所有内容也会被关闭。所以OP实际上想要从另一个Presenter呈现视图控制器,以便可以关闭当前的视图控制器。 - Radu Simionescu
但即使使用那个方法,如果你在呈现视图控制器后将它解散,那么所有它呈现的内容也会被解散。啊哈,所以编译器基本上是在说“你在做什么很蠢。你刚刚撤销了上一行代码(作为VC1,我将解散自己和我呈现的任何内容)。不要这样做”,对吗? - mfaani
OP从编译器得到了警告。这就是我的意思。 - mfaani
显示剩余3条评论

5

Radu Simionescu - 做得很棒!以下是针对Swift爱好者的解决方案:

@IBAction func showSecondControlerAndCloseCurrentOne(sender: UIButton) {
    let secondViewController = storyboard?.instantiateViewControllerWithIdentifier("ConrollerStoryboardID") as UIViewControllerClass // change it as You need it
    var presentingVC = self.presentingViewController
    self.dismissViewControllerAnimated(false, completion: { () -> Void   in
        presentingVC!.presentViewController(secondViewController, animated: true, completion: nil)
    })
}

这让我有点沮丧,因为它实际上是有效的...我不明白为什么该块没有捕获“self.presentingViewController”,需要一个强引用,即“var presentingVC”..无论如何,这个有效。谢谢 - emdog4

1

我已经通过使用UINavigationController来解决这个问题。在MainVC中,当呈现VC1时。

let vc1 = VC1()
let navigationVC = UINavigationController(rootViewController: vc1)
self.present(navigationVC, animated: true, completion: nil)

在VC1中,当我想同时显示VC2并解除VC1(只需一个动画)时,我可以通过进行推送动画来实现。
let vc2 = VC2()
self.navigationController?.setViewControllers([vc2], animated: true)

在VC2中,当关闭视图控制器时,通常我们可以使用:

self.dismiss(animated: true, completion: nil)

1

我想要的是:

MapVC 是一个全屏的地图。

当我按下一个按钮时,它会在地图上方打开 PopupVC(不是全屏)。

当我在 PopupVC 中按下一个按钮时,它会返回到 MapVC,并且我希望执行 viewDidAppear。

我做了这个:

MapVC.m:在按钮操作中,编程地进行了一个segue,并设置了delegate。

- (void) buttonMapAction{
   PopupVC *popvc = [self.storyboard instantiateViewControllerWithIdentifier:@"popup"];
   popvc.delegate = self;
   [self presentViewController:popvc animated:YES completion:nil];
}

- (void)dismissAndPresentMap {
  [self dismissViewControllerAnimated:NO completion:^{
    NSLog(@"dismissAndPresentMap");
    //When returns of the other view I call viewDidAppear but you can call to other functions
    [self viewDidAppear:YES];
  }];
}

在PopupVC.h文件的@interface之前,添加协议。
@protocol PopupVCProtocol <NSObject>
- (void)dismissAndPresentMap;
@end

在 @interface 后面,有一个新的属性。
@property (nonatomic,weak) id <PopupVCProtocol> delegate;

PopupVC.m:

- (void) buttonPopupAction{
  //jump to dismissAndPresentMap on Map view
  [self.delegate dismissAndPresentMap];
}

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