警告:尝试在视图不在窗口层次结构中的ViewController上显示ViewController

时间:2013-05-05 11:37:58

标签: iphone ios objective-c

我已经查看过相关问题,但没有解决我的问题。

我正在尝试连续使用dismissViewControllerAnimated:animated:completionpresentViewControllerAnimated:animated:completion。使用故事板,我通过部分卷曲动画模拟呈现InfoController。

部分卷曲在InfoController上显示我想要发起MFMailComposeViewController的按钮。因为部分卷曲部分隐藏了MFMailComposeViewController,所以我首先要通过取消动画部分卷曲来解除InfoController。然后我想让MFMailComposeViewController动画。

目前,当我尝试这个时,部分卷曲没有动画,但MFMailComposeViewController没有出现。我也有一个警告:

  

警告:尝试提供MFMailComposeViewController:on   InfoController:其视图不在窗口层次结构中!

InfoController.h:

#import <UIKit/UIKit.h>
#import <MessageUI/MessageUI.h>
#import <MessageUI/MFMailComposeViewController.h>

@interface InfoController : UIViewController <MFMailComposeViewControllerDelegate>

@property (weak, nonatomic) IBOutlet UIButton *emailMeButton;

-(IBAction)emailMe:(id)sender;

@end

InfoController.m

#import "InfoController.h"

@interface InfoController ()

@end

@implementation InfoController 

- (void)viewDidLoad
{
    [super viewDidLoad];
}

- (IBAction)emailMe:(id)sender {
    [self dismissViewControllerAnimated:YES completion:^{
        [self sendMeMail];
    }];
}

- (void)sendMeMail {
MFMailComposeViewController *mailController = [[MFMailComposeViewController alloc] init];
if([MFMailComposeViewController canSendMail]){
    if(mailController)
    {
        NSLog(@"%@", self); // This returns InfoController 
        mailController.mailComposeDelegate = self;
        [mailController setSubject:@"I have an issue"];
        [mailController setMessageBody:@"My issue is ...." isHTML:YES];
        [self presentViewController:mailController animated:YES completion:nil];
    }
}
}

- (void)mailComposeController:(MFMailComposeViewController*)controller
          didFinishWithResult:(MFMailComposeResult)result
                        error:(NSError*)error;
{
    if (result == MFMailComposeResultSent) {
        NSLog(@"It's sent!");
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

此外,如果我在[self dismissViewControllerAnimated:YES completion:^{}];中注释掉(IBAction)emailMeMFMailComposeViewController会动画,但它会部分隐藏在部分卷曲之后。我怎样才能首先解除卷曲然后在MFMailComposeViewController

中制作动画

非常感谢!

修改:如果我发表评论[self dismissViewControllerAnimated:YES completion:^{}];

,则视图的下方图片

enter image description here

5 个答案:

答案 0 :(得分:19)

视图控制器之间的通信问题是由于父子视图控制器关系不明确而导致的......如果不使用协议和委派,这将无法正常工作。

经验法则是:

  • 父母知道他们的孩子,但孩子不需要了解他们的父母。

(听起来很无情,但如果你想的话,这是有意义的。)

转换为ViewController关系:呈现视图控制器需要了解其子视图控制器,但子视图控制器必须不知道关于其父(呈现)视图控制器:子视图控制器使用其委托将消息发回给他们(未知)的父母。

如果必须在标头中添加@Class声明来修复链式#import编译器警告,那么您就知道出了问题。交叉引用总是一件坏事(顺便说一下,这也是委托应该总是(赋值)和永远(强)的原因,因为这会导致交叉引用循环和一组僵尸)


所以,让我们看一下你项目的这些关系:

正如您所说,我假设调用控制器名为 MainController 。所以我们有:

  • 主控制器,父控制器,拥有和呈现InfoController
  • 一个InfoController(部分显示在MainController下面),拥有并呈现:
  • MailComposer,由于它将显示在MainController下方,因此无法显示。

所以你想拥有这个:

  • 主控制器,父母,拥有并呈现InfoController&amp; MFMailController
  • InfoController(部分显示在MainController下面)
  • a&#34; Email-Button&#34;在InfoController的视图中。单击它将通知MainController(它的未知委托)它应该关闭InfoController(本身)并呈现MailComposer
  • 将由MainController而非InfoController拥有(显示和解除)的MailComposer

1。 InfoController:定义@protocol InfoControllerDelegate:

子控制器定义了一个协议,并且有一个符合其协议的未指定类型的委托(换句话说:委托可以是任何对象,但必须有这一个方法)

@protocol InfoControllerDelegate
- (void)returnAndSendMail;
@end

@interface InfoControllerDelegate : UIViewController // …

@property (assign) id<InfoControllerDelegate> delegate

// ...

@end

2。 MainController拥有并创建InfoController和MFMailController

...并且MainController同时采用InfoControllerDelegate和MFMailComposeDelegate协议,因此它可以再次关闭MFMailComposer(注意,这并不是,并且可能不应该是强大的属性,只是显示这个在这里说清楚)

@interface MainController <InfoControllerDelegate, MFMailComposeViewControllerDelegate>

@property (strong) InfoController *infoController;
@property (strong) MFMailComposeViewController *mailComposer;

第3。 MainController提供其InfoViewController并将其自身设置为委托

// however you get the reference to InfoController, just assuming it's there
infoController.delegate = self;
[self presentViewController:infoController animated:YES completion:nil];

&#39; infoController.delegate = self&#39;是至关重要的一步。这使infoController有可能在不知道ObjectType(Class)的情况下将消息发送回MainController。不需要#import。它只知道,它是一个具有方法-returnAndSendMail的对象;这就是我们需要知道的全部内容。

通常你会用alloc / init创建你的viewController并让它懒洋洋地加载它的xib。 或者,如果您正在使用Storyboards和Segues,您可能想拦截segue(在MainController中)以便以编程方式设置委托:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // hook in the segue to set the delegate of the target
    if([segue.identifier isEqualToString:@"infoControllerSegue"]) {
        InfoController *infoController = (InfoController*)segue.destinationViewController;
        infoController.delegate = self;
    }
}

4。在InfoController中,按下电子邮件按钮:

按下eMail按钮时,将调用委托(MainController)。请注意,self.delegate是MainController是不相关的,它只是具有此方法的相关性-returnAndSendMail

- (IBAction)sendEmailButtonPressed:(id)sender {
    // this method dismisses ourself and sends an eMail
    [self.delegate returnAndSendMail];
}

...和这里(在MainController中!),你将解散InfoController(清理,因为它是MainController的责任)并呈现MFMailController:< / p>

- (void)returnAndSendMail {
    // dismiss the InfoController (close revealing page)
    [self dismissViewControllerAnimated:YES completion:^{
        // and present MFMailController from MainController
        self.mailComposer.delegate = self;
        [self presentViewController:self.mailComposer animated:YES completion:nil];
    }];
}

所以,您使用MFMailController做的事情几乎与InfoController相同。两者都有他们未知的代表,所以他们可以回复,如果他们这样做,你可以解雇他们并继续你应该做的任何事情。

备注

    不应从子视图控制器调用
  • -dismissViewControllerAnimated:completion:。在文档中,它说:&#34; 呈现视图控制器负责解除它所呈现的视图控制器。&#34;。这就是我们仍然需要委托的原因。它很有用,因为父母的关系和责任很重要!确实。你不能创造一些东西,然后留下它。嗯,你可以,但你不应该。
  • 如果您不使用透露的视图控制器动画,您可以链接这些Parent(采用子协议) - Child(为父级定义协议并为孙子采用协议) - Grandchild(为...定义协议) / LI>
  • 再次说明:一个MainController拥有并提交 all 子viewController的设计实际上是一个糟糕的设计。因此,所提出的解决方案是关于协议和通信,而不是将所有内容放在一个MainController
  • 我不认为块作为编码技术使我们无需定义关系并宣布协议
  • 希望有所帮助

答案 1 :(得分:3)

当您呈现viewController时,您要呈现来自的viewController需要位于视图层次结构中。这两个VC在其属性presentingViewControllerpresentedViewController中保持彼此指针,因此两个控制器都需要在内存中。

通过解雇然后从被解雇的视图控制器运行呈现代码,您正在打破这种关系。

相反,您应该在mailController被解雇后,在显示InfoController的viewController中展示您的infoController

执行此操作的传统方式是通过委托回调到底层的viewController,然后处理解雇和呈现的两个步骤。但现在我们使用块..

将您的sendMeMail方法移至显示infoController

的VC中

然后 - 在infoController - 你可以在完成块中调用它......

- (IBAction)emailMe:(id)sender {
    UIViewController* presentingVC = self.presentingViewController;
    [presentingVC dismissViewControllerAnimated:YES completion:^{
      if ([presentingVC respondsToSelector:@selector(sendMeMail)])
          [presentingVC performSelector:@selector(sendMeMail) 
                             withObject:nil];
    }];
}

(您需要获取一个指向self.presentingViewController的本地指针,因为在控制器被解除后您无法引用该属性)

或者通过将infoController代码放在完成块中来保留sendMeMail中的所有代码:

- (IBAction)emailMe:(id)sender {
    UIViewController* presentingVC = self.presentingViewController;
    [presentingVC dismissViewControllerAnimated:YES completion:^{
        MFMailComposeViewController *mailController = 
            [[MFMailComposeViewController alloc] init];
        if([MFMailComposeViewController canSendMail]){
            if(mailController) {
                NSLog(@"%@", self); // This returns InfoController 
                mailController.mailComposeDelegate = presentingVC; //edited
                [mailController setSubject:@"I have an issue"];
                [mailController setMessageBody:@"My issue is ...." isHTML:YES];
                [presentingVC presentViewController:mailController 
                                           animated:YES 
                                         completion:nil];
                }
            }
    }];
}

更新/编辑
如果将所有代码放在完成块中,则应将mailController的mailComposeDelegate设置为presentingVC,而不是self。然后在呈现viewController中处理委托方法。

更新2
@Auro使用委托方法提供了详细的解决方案,并在他的评论中指出,这最好地表达了角色的分离。我的传统主义者同意,我认为dismissViewController:animated:completion是一个kludgy and easily misunderstood API。

Apple的文档have this to say

  

解雇呈现的视图控制器

     

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

请注意,他们在这里甚至没有提到dismissViewController:animated:completion:,好像他们对自己的API不太尊重。

但代表团似乎是一个人们经常挣扎的问题:并且可能需要a lengthy answer ......我认为这是苹果如此努力推动封锁的原因之一。在代码只需要在一个地方执行的情况下,代理模式通常被初学者认为是对一个看似简单的问题的过于复杂的解决方案。

我认为,如果你正在学习这些东西,最好的答案就是两种方式实现它。然后你将真正掌握游戏中的设计模式。

答案 2 :(得分:1)

关于此,

- (IBAction)emailMe:(id)sender {
    [self dismissViewControllerAnimated:YES completion:^{
        [self sendMeMail];
    }];
}

在解除 self viewController后,您无法从self呈现视图控制器。

然后你能做什么?

1)更改按钮按下方法

- (IBAction)emailMe:(id)sender {
    [self sendMeMail];
}

2)当mailViewController被解除时,您可以解除自身的viewController。

- (void)mailComposeController:(MFMailComposeViewController*)controller
          didFinishWithResult:(MFMailComposeResult)result
                        error:(NSError*)error;
{
    if (result == MFMailComposeResultSent) {
        NSLog(@"It's sent!");
    }
    [controller dismissViewControllerAnimated:NO completion:^ {
        [self dismissViewControllerAnimated:YES completion:nil];
    }];

}

答案 3 :(得分:0)

如果您正在使用故事板,请尝试检查您在segue上使用的转换类型。解决您以模态方式转换的视图控制器层时,您将遇到问题。这可能是您的问题的根源。尝试将它们切换为推动。 enter image description here虽然您可以编写代理来完成解雇视图控制器,但实际上并不需要它。这是一个过于复杂的解决方案。如果你有一个转换到几十个不同故事板的viewcontroller,你会有几十个代表控制解雇吗?这似乎不是最理想的。

答案 4 :(得分:0)

使用此代码

[[[[[UIApplication sharedApplication] delegate] window] rootViewController] presentViewController:composer                                                                                                    animated:YES completion:nil];

而不是

[self presentViewController:picker animated:YES completion:NULL];