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

时间:2013-02-16 06:06:18

标签: ios objective-c uiviewcontroller presentviewcontroller dismissviewcontroller

关于如何通过显示和解除多个视图控制器解决问题,我在最后一周打破了我的脑袋。我创建了一个示例项目并直接从项目中粘贴代码。我有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上单击按钮时应该返回到主控制器。没有长时间运行的代码,循环或任何计时器。只是裸骨调用来查看控制器。

6 个答案:

答案 0 :(得分:186)

这一行:

[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文件中,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-&gt; VC2移动,因此您可能只希望其中一个过渡动画。

更新

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

在Apple的 View Controller编程指南中,他们有this to say

  

解雇呈现的视图控制器

     

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

如果您真的想通过自己想要实现的目标,以及如何实现目标,那么您将意识到,如果您不想要的话,将MainViewController发送给所有工作的消息是唯一合理的方法。使用NavigationController。如果你使用NavController,实际上即使没有明确地,也要委托navController完成所有的工作。需要有一些对象来跟踪VC导航的内容,你需要一些与之通信的方法,无论你是什么做。

在实践中,Apple的建议有点极端......在正常情况下,您不需要制作专门的代表和方法,您可以依赖[self presentingViewController] dismissViewControllerAnimated: - 它&#39 ; s在像你这样的情况下,你希望你的解雇对你需要照顾的远程对象产生其他影响。

在没有所有委托麻烦的情况下,你可以想象工作......

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

}

在要求呈现控制器解雇我们之后,我们有一个完成块,它调用presentationViewController中的方法来调用VC2。不需要代表。 (块的一大卖点是它们在这些情况下减少了对代表的需求)。但是在这种情况下,有一些事情会妨碍......

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

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

<强> UPDATE2

在你的评论中,你已经设法通过在VC2的解雇按钮处理程序中使用它来使其工作:

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

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

紧耦合
您正在将viewController结构连接在一起。例如,如果要在mainVC之前插入新的viewController,则所需的行为将会中断(您将导航到前一个)。在VC1中你还必须#import VC2。因此,您有很多相互依赖关系,这会破坏OOP / MVC目标。

使用代理人,VC1和VC2都不需要了解有关mainVC或它的前提的任何内容,因此我们保持所有松散耦合和模块化。

内存
VC1还没有消失,你仍然有两个指针:

  • mainVC&#39; presentedViewController属性
  • VC2&#39; 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到达的,但它是你最好的没有一点明确的授权就会得到。

答案 1 :(得分:9)

我认为您误解了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解散后,您不应该对它进行任何引用。

答案 2 :(得分:9)

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

  1. 基于Apple's documentation和代工厂 上面的解释(纠正一些错误),presentViewController 使用委托设计模式的版本:
  2. <强> 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版本使用委托设计模式:
    2. <强> 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 {
      
      }
      

答案 3 :(得分: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)
    })
}

答案 4 :(得分:0)

我想要这个:

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

答案 5 :(得分:0)

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