【问题标题】:modal View controllers - how to display and dismiss模态视图控制器 - 如何显示和关闭
【发布时间】:2013-02-01 04:25:50
【问题描述】:

在过去的一周里,我一直在思考如何解决显示和关闭多个视图控制器的问题。我创建了一个示例项目并直接从项目中粘贴代码。我有 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 上的按钮时应该返回到主控制器。没有长时间运行的代码、循环或任何计时器。只是简单地调用视图控制器。

【问题讨论】:

    标签: ios objective-c uiviewcontroller presentviewcontroller dismissviewcontroller


    【解决方案1】:

    这一行:

    [self dismissViewControllerAnimated:YES completion:nil];
    

    不是向自己发送消息,它实际上是向其呈现的 VC 发送消息,要求它进行解散。当你呈现一个 VC 时,你在呈现的 VC 和被呈现的 VC 之间创建了一种关系。所以你不应该在呈现的时候破坏正在呈现的 VC(呈现的 VC 不能发回驳回消息……)。由于您没有真正考虑到它,因此您使应用程序处于混乱状态。看我的回答Dismissing a Presented View Controller 其中我推荐这个方法写得更清楚:

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

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

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

    @protocol ViewController1Protocol <NSObject>
    
        - (void)dismissAndPresentVC2;
    
    @end
    

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

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

    在 VC1 .m 文件中,关闭按钮方法应该调用委托方法

    - (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 移动,因此您可能只希望对其中一个过渡进行动画处理。

    更新

    在您的 cmets 中,您对实现看似简单的事情所需的复杂性表示惊讶。我向你保证,这种委托模式对于 Objective-C 和 Cocoa 的大部分内容来说都是如此重要,而且这个例子是你能得到的最简单的例子,你真的应该努力适应它。

    在 Apple 的 View Controller Programming Guide 中,他们有 this to say:

    关闭呈现的视图控制器

    当需要关闭呈现的视图控制器时,首选方法是让呈现的视图控制器关闭它。换句话说,只要有可能,呈现视图控制器的同一个视图控制器也应该负责解除它。尽管有几种技术可以通知呈现视图控制器其呈现的视图控制器应该被解除,但首选技术是委托。有关详细信息,请参阅“使用委托与其他控制器通信”。

    如果你真的仔细考虑过你想要实现什么,以及你将如何去做,你会意识到给你的 MainViewController 发送消息来完成所有的工作是唯一合乎逻辑的出路,因为你不想使用导航控制器。如果您确实使用 NavController,实际上您是在“委派”(即使不是明确地)给 navController 来完成所有工作。需要有 some 对象来集中跟踪您的 VC 导航发生的情况,并且无论您做什么,都需要 some 方法与之通信。 p>

    实际上,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 时,两个对象都会消失

    这是一种相当冗长的说法 - 请使用代表。如果有帮助,我在这里对模式进行了另一个简短的描述:
    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 到达的,但它是最好的,没有一点显式委托。

    【讨论】:

    • 似乎很复杂。我试图跟随并复制到点,但在中间迷路了。有没有其他方法可以实现这一目标?我还想在应用程序委托中添加,主控制器设置为根视图控制器。我不想使用导航控制器,但想知道为什么实现起来如此复杂。总而言之,当应用程序启动时,我展示了一个带有 2 个按钮的主视图控制器。单击第一个按钮加载 VC1。 VC1 上有一个按钮,点击它应该会加载 VC2,没有错误或警告,同时从内存中关闭 VC1。
    • 在 VC2 上,我有一个按钮,单击它应该会从内存中关闭 VC2,并且控件应该返回主控制器而不是 VC1。
    • @Hema,我完全理解您的要求,并向您保证这正确的方法。我已经用更多信息更新了我的答案,希望对您有所帮助。如果您尝试了我的方法并卡住了,请提出一个新问题,确切说明什么不起作用,以便我们提供帮助。为了清楚起见,您还可以链接回此问题。
    • 您好他是:感谢您的洞察力。我也在谈论另一个线程(原始线程),并且刚刚从那里提到的建议中发布了一个 sn-p。我正在尝试所有专家的答案来确定这个问题。网址在这里:stackoverflow.com/questions/14840318/…
    • @Honey - 也许是这样,但该声明是对一段“想象的”伪代码的修辞回答。我想说的不是保留周期陷阱,而是教育提问者为什么委托是一种有价值的设计模式(顺便避免了这个问题)。我认为这是一个误导性的争论——问题是关于模态 VC,但答案的价值主要在于它对委托模式的解释,使用问题和 OP 的明显挫折作为催化剂。感谢您的关注(和您的编辑)!!
    【解决方案2】:

    Swift 中的示例,上面是代工厂的解释和 Apple 的文档:

    1. 基于Apple's documentation 和代工厂的 上面的解释(纠正一些错误),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 对吗?
    【解决方案3】:

    我认为您误解了有关 iOS 模态视图控制器的一些核心概念。当您关闭 VC1 时,VC1 提供的任何视图控制器也会被关闭。 Apple 旨在让模态视图控制器以堆叠方式流动 - 在您的情况下,VC2 由 VC1 提供。一旦您从 VC1 呈现 VC2,您就会解雇 VC1,所以这完全是一团糟。 为了实现您想要的,buttonPressedFromVC1 应该在 VC1 自行关闭后立即让 mainVC 出现 VC2。我认为这可以在没有代表的情况下实现。类似的东西:

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

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

    【讨论】:

    • 这么简单!我希望其他人会向下滚动到您的答案,而不是停留在最上面的帖子。
    • 在 OP 的代码中,为什么 [self dismiss...] 不发生 [self present...] 完成后?这不是异步发生的事情
    • @Honey 实际上,在调用 presentViewController 时会发生一些异步事件——这就是它具有完成处理程序的原因。但即使使用它,如果您在呈现视图控制器呈现某些内容后关闭呈现视图控制器,它呈现的所有内容也会被解散。所以 OP 实际上想要从另一个演示者那里呈现视图控制器,以便它可以关闭当前的视图控制器
    • 但即使使用它,如果您在呈现某些内容后关闭呈现的视图控制器,它呈现的所有内容也会被解除...啊哈,所以编译器基本上是说“你所做的很愚蠢。你只是撤消了你之前的代码行(作为 VC1,我将解雇我自己和我所呈现的任何内容)。不要这样做”对吗?
    • 编译器不会“说”任何关于它的东西,而且执行它时也可能不会崩溃,只是它会以程序员不期望的方式运行
    【解决方案4】:

    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” ..无论如何,这行得通。谢谢
    【解决方案5】:

    我想要这个:

    MapVC 是全屏地图。

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

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

    我这样做了:

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

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

    【讨论】:

      【解决方案6】:

      我在演示时使用 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)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-01-01
        • 2010-12-04
        • 1970-01-01
        相关资源
        最近更新 更多