模态视图控制器-如何显示和关闭


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中,我有另一个按钮应该打开VC2,同时关闭VC1。

- (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。

主视图控制器上的另一个按钮也应该能够直接绕过VC1来显示VC2,并且在VC2上单击一个按钮时应回到主控制器。没有长时间运行的代码,循环或任何计时器。只需裸露骨头即可调用视图控制器。

Answers:


189

这行:

[self dismissViewControllerAnimated:YES completion:nil];

不是在向自己发送消息,而是在向其呈现的VC发送消息,要求其执行关闭操作。呈现VC时,将在呈现的VC和呈现的VC之间创建一种关系。因此,您不应在演示时破坏演示VC(演示的VC无法发回该关闭消息…)。由于您并未真正考虑到它,因此使应用程序处于混乱状态。请参阅我的回答“解散Presented View Controller ”,我建议在其中更清晰地编写此方法:

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

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

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

@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时将其设置为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来说至关重要,而这个示例是您所能获得的最简单的示例,因此您确实应该努力使其满意。

在Apple的《View Controller编程指南》中,他们这样说

消除呈现的视图控制器

当需要解散呈现的视图控制器时,首选方法是让呈现的视图控制器将其解雇。换句话说,只要有可能,呈现视图控制器的同一视图控制器也应负责解散它。尽管有多种技术可以通知呈现视图控制器其呈现的视图控制器应被取消,但首选技术是委派。有关更多信息,请参见“使用委托与其他控制器进行通信”。

如果您真正考虑了要实现的目标以及发展的方式,您将意识到向MainViewController传递消息以完成所有工作是您不想使用NavigationController的唯一逻辑出路。如果您确实使用了NavController,则实际上是在“委派”(即使不是显式地)navController来完成所有工作。需要有一个对象来始终跟踪VC导航的运行情况,并且无论您做什么,都需要某种与之通信的方法。

实际上,Apple的建议有点极端……在正常情况下,您无需建立专门的委托和方法,就可以依靠[self presentingViewController] dismissViewControllerAnimated:-在这种情况下,您希望您的解雇对遥控器产生其他影响您需要注意的物品。

可以想象在没有所有代表麻烦的情况下工作。

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

}

在要求演示控制器解雇我们之后,我们有一个完成模块,该模块调用presentingViewController中的方法来调用VC2。无需代表。(块的一大卖点是,在这种情况下,它们减少了对代表的需求)。但是,在这种情况下,有几件事会妨碍您...

  • 在VC1中,您不知道mainVC实现了该方法present2-您最终会遇到难以调试的错误或崩溃。代表们可以帮助您避免这种情况。
  • 一旦VC1被解雇,执行完成块就不是真正的事情了……或者是吗?self.presentingViewController不再有意义吗?您不知道(我也不知道)...与一位代表一起,您没有这种不确定性。
  • 当我尝试运行此方法时,它只是挂起而没有警告或错误。

所以,请...花时间学习授权!

更新2

在您的评论中,您设法通过在VC2的关闭按钮处理程序中使用此命令来使其工作:

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

这当然要简单得多,但是会给您带来许多问题。

紧密耦合
您正在将viewController结构紧密地连接在一起。例如,如果要在mainVC之前插入一个新的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!

当您尝试关闭显示的VC时,逻辑崩溃了,其中VC2是显示的VC。第二条消息并没有真正执行-也许发生了一些事情,但是您仍然有两个指针指向您认为已经摆脱的对象。(编辑-我已经检查了这一点,它还不错,回到mainVC时两个对象都消失了

这是一个相当漫长的说法-请使用委托。如果有帮助,我在这里对模式进行了另一个简短的描述:
在构造函数中传递控制器总是不好的做法吗?

更新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

在VC2上,我有一个按钮,单击它应该从内存中退出VC2,并且控件应返回主控制器而不是VC1。
Hema

@Hema,我完全理解您的要求,并向您保证这正确的方法。我已经用更多信息更新了我的答案,希望能对您有所帮助。如果您尝试了我的方法而陷入困境,请提出一个新问题,以确切说明什么不起作用,以便我们提供帮助。为了清楚起见,您也可以链接回该问题。
铸造厂

嗨,他是:谢谢您的见解。我也在谈论另一个线程(原始线程),只是从那里提到的建议中发布了一个片段。我正在尝试所有专家解答来确定此问题。URL在这里:stackoverflow.com/questions/14840318/…–
Hema

1
@Honey-也许是这样,但该声明是对一段“想象中的”伪代码的反问。我要讲的重点不是保留周期陷阱,而是教育提问者为什么委托是一种有价值的设计模式(顺便避免了这个问题)。我认为这是误导性的争论-问题是关于模式VC,但是答案的价值主要在于使用问题和OP的明显挫败感来推动代理模式的解释。感谢您的关注(和您的修改)!
铸造厂

12

Swift中的示例,描绘了上述代工厂的说明和Apple的文档:

  1. 基于Apple的文档和上述铸造厂的说明(纠正了一些错误),使用委托设计模式的presentViewController版本:

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对吗?
亲爱的

10

我认为您误解了有关iOS模式视图控制器的一些核心概念。当您关闭VC1时,任何由VC1呈现的视图控制器也将被关闭。Apple希望模式视图控制器以堆叠的方式流动-在您的情况下,VC2由VC1提供。一旦从VC1提供VC2,就将VC1解散,这是一团糟。为了实现所需的功能,在VC1关闭后,buttonPressedFromVC1应该立即使mainVC出现在VC2中。我认为,没有代表就可以实现这一目标。大致情况:

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

请注意,self.presentingViewController存储在其他变量中,因为在vc1关闭自身之后,您不应对其进行任何引用。


1
很简单!我希望其他人可以向下滚动到您的答案,而不是停留在顶部。
Ryan Loggerythm 2015年

在OP的代码中,为什么完成[self dismiss...]发生?这不是异步的事情发生 [self present...]
Honey,

1
@Honey实际上,调用presentViewController时发生了一些异步事件-这就是为什么它具有完成处理程序的原因。但是即使使用它,如果您在呈现某个内容后关闭呈现视图控制器,则呈现的所有内容也会被关闭。因此,OP实际上实际上想从另一位演示者那里演示
视图控制器

但是即使使用了该方法,如果您在呈现出某个东西之后关闭呈现视图控制器,它呈现的所有内容也会被消除...啊哈,所以编译器基本上是在说“您的操作很愚蠢。您只需撤消上一个代码行(作为VC1,我将解雇自己以及所提交的内容)。不要这样做吗?
亲爱的

编译器不会“说”任何事情,在执行该操作时也可能不会崩溃,只是它的行为方式是程序员所不希望的
Radu Simionescu

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”。thx
emdog4

1

我想要这个:

MapVC是全屏地图。

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

当我在PopupVC中按下按钮时,它返回到MapVC,然后我要执行viewDidAppear。

我这样做:

MapVC.m:在按钮操作中,以编程方式进行搜索,并设置委托

- (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];
}

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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.