从子Modal VC传递数据到父视图控制器的最佳方法是什么?

10

如何从子模态视图传递数据到父视图控制器?

我在我的iPad应用程序中有一个子模态登录屏幕,我想将用户信息传回父分割视图控制器。

我考虑使用NSNotification,但我不确定这是否是将数据传回父级的最简单/最有效的方法。

谢谢! Alan


我经常使用NSNotifications来实现这个目的。在这种情况下,我不喜欢使用协议,因为子类需要知道父类的方法,但这只是一种设计偏好的问题... - Mike M
2个回答

14

我建议,正如iPatel所做的那样, 使用委托来解决你的问题。父视图控制器和登录视图控制器之间的关系使得这种模式非常适合。当一个对象创建另一个对象以履行特定职责时,应考虑使用委托作为让创建的对象与创建者通信的一种方式。选择委托的一个特别有说服力的理由是,如果要完成的任务可能涉及多个步骤,需要对象之间高水平的交互,你可以看NSURLConnectionDelegate 协议作为一个例子。连接到URL是一个复杂的任务,涉及处理响应、满足认证挑战、保存下载数据和处理错误等阶段,连接和代理一起处理连接的生命周期。

正如您可能已经注意到的那样,在Objective-C中,协议被用来实现委托,而不会让被创建的对象(在这种情况下是您的登录视图控制器)与创建它的对象(父视图控制器)紧密耦合。然后,登录视图控制器可以与任何能够接收其协议定义的消息的对象进行交互,而不是依赖于任何特定类的实现。如果明天您收到一个要求允许任何视图控制器显示登录视图的需求,那么登录视图控制器就不需要改变。您的其他视图控制器可以实现其委托协议,创建和呈现登录视图,并将自己分配为委托,而登录视图控制器永远不会知道它们的存在。

您在Stack Overflow上找到的一些委托示例可能非常令人困惑,并且与内置框架中的内容非常不同。必须仔细选择协议的名称和接口,以及分配给每个对象的职责,以便最大化代码重用并实现代码目标。

首先,您应该查看内置框架中的许多委托协议,以了解在代码中表达时关系的外观。以下是另一个基于您的登录用例的小例子。我希望您会发现委托的目的很清楚,并且涉及到的对象的角色和责任在代码中通过它们的名称得到了明确表达。

首先,让我们看一下LoginViewController的代理协议:
#import <UIKit/UIKit.h>

@protocol LoginViewControllerDelegate;

@interface LoginViewController : UIViewController

// We choose a name here that expresses what object is doing the delegating
@property (nonatomic, weak) id<LoginViewControllerDelegate> delegate;

@end

@protocol LoginViewControllerDelegate <NSObject>

// The methods declared here are all optional
@optional

// We name the methods here in a way that explains what the purpose of each message is
// Each takes a LoginViewController as the first argument, allowing one object to serve
// as the delegate of many LoginViewControllers
- (void)loginViewControllerDidLoginSuccessfully:(LoginViewController *)lvc;
- (void)loginViewController:(LoginViewController *)lvc didFailWithError:(NSError *)error;
- (void)loginViewControllerDidReceivePasswordResetRequest:(LoginViewController *)lvc;
- (void)loginViewControllerDiDReceiveSignupRequest:(LoginViewController *)lvc;
- (BOOL)loginViewControllerShouldAllowAnonymousLogin:(LoginViewController *)lvc;

@end

登录控制器可以向其代理通信多个事件,同时还可以向其代理请求用于自定义行为的信息。它在响应用户操作时作为其实现的一部分向代理通信事件:

#import "LoginViewController.h"

@interface LoginViewController ()

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

@end

@implementation LoginViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    //  Here we ask the delegate for information used to layout the view
    BOOL anonymousLoginAllowed = NO;
    //  All our protocol methods are @optional, so we must check they are actually implemented before calling.
    if ([self.delegate respondsToSelector:@selector(loginViewControllerShouldAllowAnonymousLogin:)]) {
        // self is passed as the LoginViewController argument to the delegate methods
        // in this way our delegate can serve as the delegate of multiple login view controllers, if needed
        anonymousLoginAllowed = [self.delegate loginViewControllerShouldAllowAnonymousLogin:self];
    }
    self.anonSigninButton.hidden = !anonymousLoginAllowed;
}

- (IBAction)loginButtonAction:(UIButton *)sender
{
    // We're preteneding our password is always bad. So we assume login succeeds when allowed anonmously
    BOOL loginSuccess = [self isAnonymousLoginEnabled];
    NSError *loginError = [self isAnonymousLoginEnabled] ? nil : [NSError errorWithDomain:@"domain" code:0 userInfo:nil];

    //  Fake concurrency
    double delayInSeconds = 1.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        //  Notify delegate of failure or success
        if (loginSuccess) {
            if ([self.delegate respondsToSelector:@selector(loginViewControllerDidLoginSuccessfully:)]) {
                [self.delegate loginViewControllerDidLoginSuccessfully:self];
            }
        }
        else {
            if ([self.delegate respondsToSelector:@selector(loginViewController:didFailWithError:)]) {
                [self.delegate loginViewController:self didFailWithError:loginError];
            }
        }
    });
}

- (IBAction)forgotPasswordButtonAction:(id)sender
{
    //  Notify delegate to handle forgotten password request.
    if ([self.delegate respondsToSelector:@selector(loginViewControllerDidReceivePasswordResetRequest:)]) {
        [self.delegate loginViewControllerDidReceivePasswordResetRequest:self];
    }
}

- (IBAction)signupButtonAction:(id)sender
{
    //  Notify delegate to handle signup request.
    if ([self.delegate respondsToSelector:@selector(loginViewControllerDiDReceiveSignupRequest:)]) {
        [self.delegate loginViewControllerDiDReceiveSignupRequest:self];
    }
}

- (BOOL)isAnonymousLoginEnabled
{
    BOOL anonymousLoginAllowed = NO;

    if ([self.delegate respondsToSelector:@selector(loginViewControllerShouldAllowAnonymousLogin:)]) {
        anonymousLoginAllowed = [self.delegate loginViewControllerShouldAllowAnonymousLogin:self];
    }
    return  anonymousLoginAllowed;
}

@end

主视图控制器实例化并呈现一个登录视图控制器,并处理其委托消息:
#import "MainViewController.h"
#import "LoginViewController.h"

#define LOGGED_IN NO

@interface MainViewController () <LoginViewControllerDelegate>

@end

@implementation MainViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    //  Fake loading time to show the modal cleanly
    if (!LOGGED_IN) {
        double delayInSeconds = 1.0;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            //  Create a login view controller, assign its delegate, and present it
            LoginViewController *lvc = [[LoginViewController alloc] init];
            lvc.delegate = self;
            [self presentViewController:lvc animated:YES completion:^{
                NSLog(@"modal completion finished.");
            }];
        });
    }
}

#pragma mark - LoginViewControllerDelegate


- (void)loginViewControllerDidLoginSuccessfully:(LoginViewController *)lvc
{
    NSLog(@"Login VC delegate - Login success!");
    [self dismissViewControllerAnimated:YES completion:NULL];
}

- (void)loginViewController:(LoginViewController *)lvc didFailWithError:(NSError *)error
{
    // Maybe show an alert...
    // UIAlertView *alert = ...
}

- (void)loginViewControllerDidReceivePasswordResetRequest:(LoginViewController *)lvc
{
    // Take the user to safari to reset password maybe
     NSLog(@"Login VC delegate - password reset!");
}

- (void)loginViewControllerDiDReceiveSignupRequest:(LoginViewController *)lvc
{
    // Take the user to safari to open signup form maybe
    NSLog(@"Login VC delegate - signup requested!");
}

- (BOOL)loginViewControllerShouldAllowAnonymousLogin:(LoginViewController *)lvc
{
    return YES;
}

@end

登录过程有时可能是一个复杂的交互过程,因此我建议您认真考虑使用委托而不是通知。但是,可能会出现一个问题,就是委托必须是单个对象。如果您需要多个不同的对象知道登录视图控制器的进度和状态,则可能需要使用通知。特别是如果登录过程可以被约束为非常简单,以一种不需要超过传递单向消息和数据的交互方式进行,则通知可以成为可行的选择。您可以在 userInfo 属性中传递任意变量的通知回来,这是一个 NSDictionary,其中包含您决定放入其中的内容。通知会影响性能,但我了解到只有在观察者数目达到数百时才会发生。即便如此,在我的看法中它仍然不是最自然的选择,因为您有父对象(更或者说是控制子对象的生命周期的对象)从第三方对象那里请求子对象的更新。

1
哇,我真的很感激你花时间给我做了这么详尽的解释。我现在对于创建委托以及何时使用它们有了更深入的理解。非常感谢你! - Alan
我注意到你的所有委托都返回自身。所有委托通常都会返回自身吗? - Alan
1
@Alan,它们实际上并没有返回 self,而是将 self 作为参数传递给方法。你经常会看到委托协议在其方法中有一个委托对象的参数。例如,看一下 UITableViewDelegate,每个方法的第一个参数都是 UITableView。委托对象在委托方法调用中传递自己,以便委托可以作为多个这样的对象的委托,并且有一种区分谁在调用该方法的方法。顺便说一句,那是一个很好的问题,我应该明确地将其添加到我的答案中。 - Carl Veazey
如果我必须通过多个视图控制器在模态视图中获取用户输入,其中模态视图是UINavigationController,那么我该如何使用上述内容? - NetDeveloper
如果模态视图是 UINavigationController,则使用导航控制器的根视图控制器作为委托类。 - Carl Veazey

5
你可以通过使用协议来获取它,这是最好的方法。
我将为您提供创建协议的基本思路。
另外,请阅读以下问题:如何在Objective-C中创建委托? 以下代码为您提供了关于协议的基本思路,在下面的代码中,您可以从MasterViewController获取按钮标题到DetailViewController
#DetailViewController.h

#import <UIKit/UIKit.h>

@protocol MasterDelegate <NSObject>
-(void) getButtonTitile:(NSString *)btnTitle;
@end


@interface DetailViewController : MasterViewController

@property (nonatomic, assign) id<MasterDelegate> customDelegate; 

#DetailViewController.m

if([self.customDelegate respondsToSelector:@selector(getButtonTitile:)])
{
          [self.customDelegate getButtonTitile:button.currentTitle];    
}

#MasterViewController.m

create obj of DetailViewController

DetailViewController *obj = [[DetailViewController alloc] init];
obj.customDelegate = self;
[self.navigationController pushViewController:reportTypeVC animated:YES];

and add delegate method in MasterViewController.m for get button title.

#pragma mark -
#pragma mark - Custom Delegate  Method

-(void) getButtonTitile:(NSString *)btnTitle;
{
    NSLog(@"%@", btnTitle);

}

太好了!我会尝试一下并回复您。谢谢。 - Alan
出于好奇,为什么这种方法比NSNotification更好? - Alan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接