9

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

我正在尝试使用dismissViewControllerAnimated:animated:completionpresentViewControllerAnimated:animated:completion连续。使用情节提要,我通过部分卷曲动画模态地呈现 InfoController。

部分卷曲显示 InfoController 上的一个按钮,我想启动MFMailComposeViewController. 因为部分卷曲部分隐藏了MFMailComposeViewController,所以我首先想通过取消部分卷曲的动画来关闭 InfoController。然后我想要MFMailComposeViewController动画。

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

警告:尝试在 InfoController 上显示 MFMailComposeViewController:其视图不在窗口层次结构中!

信息控制器.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

信息控制器.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:^{}];in (IBAction)emailMeMFMailComposeViewController动画 in 但它部分隐藏在部分卷曲后面。我怎样才能先解除卷曲,然后在MFMailComposeViewController?

非常感谢!

编辑:如果我注释掉,下图是视图的样子[self dismissViewControllerAnimated:YES completion:^{}];

在此处输入图像描述

4

5 回答 5

19

It's a communications issue between view-controllers resulting out of an unclear parent-child view-controller relationship... Without using a protocol and delegation, this won't work properly.

The rule of thumb is:

  • Parents know about their children, but children don't need to know about their parents.

(Sounds heartless, but it makes sense, if you think about it).

Translated to ViewController relationships: Presenting view controllers need to know about their child view controllers, but child view controllers must not know about their parent (presenting) view controllers: child view controllers use their delegates to send messages back to their (unknown) parents.

You know that something is wrong if you have to add @Class declarations in your headers to fix chained #import compiler warnings. Cross-references are always a bad thing (btw, that's also the reason why delegates should always be (assign) and never (strong), as this would result in a cross-reference-loop and a group of Zombies)


So, let's look at these relationships for your project:

As you didn't say, I assume the calling controller is named MainController. So we'll have:

  • A MainController, the parent, owning and presenting the InfoController
  • An InfoController (revealed partially below MainController), owning and presenting a:
  • MailComposer, which cannot be presented because it would be displayed below the MainController.

So you want to have this:

  • A MainController, the parent, owning and presenting the InfoController & MFMailController
  • An InfoController (revealed partially below MainController)
  • an "Email-Button" in the InfoController's view. On click it will inform the MainController (it's unknown delegate) that it should dismiss the InfoController (itself) and present the MailComposer
  • an MailComposer that will be owned (presented & dismissed) by the MainController and not by the InfoController

1. InfoController: Defines a @protocol InfoControllerDelegate:

The child controller defines a protocol and has a delegate of unspecified type which complies to its protocol (in other words: the delegate can be any object, but it must have this one method)

@protocol InfoControllerDelegate
- (void)returnAndSendMail;
@end

@interface InfoControllerDelegate : UIViewController // …

@property (assign) id<InfoControllerDelegate> delegate

// ...

@end

2. MainController owns and creates both InfoController and MFMailController

...and the MainController adopts both the InfoControllerDelegate and the MFMailComposeDelegate protocol, so it can dismiss the MFMailComposer again (Note, that doesn't and probably shouldn't need to be strong properties, just showing this here to make it clear)

@interface MainController <InfoControllerDelegate, MFMailComposeViewControllerDelegate>

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

3. MainController presents its InfoViewController and sets itself as the delegate

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

The 'infoController.delegate = self' is the crucial step. This gives the infoController a possibility to send a message back to the MainController without knowing it ObjectType (Class). No #import required. All it knows, it that it's an object that has the method -returnAndSendMail; and that's all we need to know.

Typically you would create your viewController with alloc/init and let it load its xib lazily. Or, if you're working with Storyboards and Segues, you probably want to intercept the segue (in MainController) in order to set the delegate programmatically:

- (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. In InfoController, the eMail button is pressed:

When the eMail button is pressed, the delegate (MainController) is called. Note that it's not relevant that self.delegate is the MainController, it's just relevant that it has this method -returnAndSendMail

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

...and here (in MainController!), you'll dismiss the InfoController (clean up because it's the responsibility of the MainController) and present the MFMailController:

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

so, what you're doing with the MFMailController is practically the same as with the InfoController. Both have their unknown delegate, so they can message back and if they do, you can dismiss them and proceed with whatever you should to do.

Notes

  • -dismissViewControllerAnimated:completion: should not be called from the child view controller. In the docs, it says: "The presenting view controller is responsible for dismissing the view controller it presented.". That's why we still need delegation. And it's useful, because the relationships and responsibilities of parents are important! Indeed. You can't create something and then just leave it be. Well, you can, but you shouldn't.
  • if you wouldn't use a revealing view controller animation, you could chain these Parent (adopting Child Protocol) - Child (defining protocol for parent and adopting protocol for grandchild) - Grandchild (defining protocol for ...
  • Again: a design where one MainController is owning and presenting all the child viewController is really a bad design. So the solution presented is about protocols and communication and not about putting everything in one MainController
  • I don't think that blocks as a coding technology free us from the need to define relationships and declare Protocols
  • Hope that helps
于 2013-05-06T10:56:38.053 回答
3

当您呈现一个 viewController 时,您正在呈现的 viewController需要位于视图层次结构中。presentingViewController两个 VC 在它们的属性和中持有指向彼此的指针presentedViewController,因此两个控制器都需要在内存中。

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

相反,您应该在关闭后mailController从显示的 viewController 中显示您的。InfoControllerinfoController

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

将您的sendMeMail方法移动到提供的 VC 中infoController

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

- (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这是一个笨拙且容易被误解的 API。

苹果的文档有这样的说法

关闭呈现的视图控制器

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

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

但授权似乎是人们经常遇到的一个问题:并且可能需要一个冗长的答案......我认为这是苹果如此努力推动区块的原因之一。在代码只需要在一个地方执行的情况下,委托模式通常被外行认为是对一个看似简单的问题的过于复杂的解决方案。

我想最好的答案是,如果你正在学习这些东西,那就是双向实施。然后,您将真正掌握正在使用的设计模式。

于 2013-05-05T12:25:33.477 回答
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];
    }];

}
于 2013-05-05T11:49:37.263 回答
0

使用此代码

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

代替

[self presentViewController:picker animated:YES completion:NULL];
于 2014-12-12T05:29:34.640 回答
0

如果您使用故事板,请尝试检查您在转场上使用的过渡类型。您将无法解除以模态方式转换的视图控制器层。这可能是您问题的根源。尝试将它们切换为 push。在此处输入图像描述尽管您可以编写委托来完成关闭视图控制器,但实际上并不需要它。这是一个过于复杂的解决方案。想象一下,如果你有一个视图控制器可以转换到几十个不同的故事板,你会有几十个代表控制解雇吗?这似乎不是最理想的。

于 2013-05-07T03:28:55.780 回答