Search code examples
iosobjective-cxcodemodalviewcontroller

Best way to pass data from Child Modal VC to the Parent View Controller?


What is the best way to pass data from a child modal view to the parent view controller?

I have a Child Modal Login Screen on my iPad app that I want to pass back user information to the parent Split View Controller.

I am thinking of using NSNotification, but I am not sure if this is the easiest/most efficient way to pass data back to the parent.

Thanks! Alan


Solution

  • I would suggest, as iPatel did, to use delegation to solve your problem. The relationship between the parent view controller and the login view controller makes this pattern appropriate. When one object creates another in order to fulfill a specific responsibility, one should consider delegation as a way to have the created object communicate with the creator. A particularly compelling reason to choose delegation would be if the task to be accomplished potentially has multiple steps that require a high level of interaction between objects. You can look at the NSURLConnectionDelegate protocol as an illustration of this. Connecting to a URL is a complex task, involving stages such as processing responses, meeting authentication challenges, saving downloaded data, and handling errors, the connection and the delegate handle this together over the lifetime of the connection.

    As you've probably noticed, in Objective-C protocols are used to achieve delegation without tightly coupling the created object (in this case your login view controller) to the object that created it (the parent view controller). The login view controller can then interact with any object that can receive the messages defined in its protocol, rather than rely on any particular class implementation. Tomorrow, if you receive a requirement to allow any view controller to show the login view, the login view controller wouldn't need to change. Your other view controllers can implement its delegate protocol, create and present the login view, and assign themselves as delegates without the login view controller ever knowing of their existence.

    Some delegation examples you'll find on Stack Overflow may be very confusing and very un-like what's found in the built-in frameworks. One must pick the names and interfaces of the protocols carefully, as well as the responsibilities assigned to each object, so that code reuse is maximized and the objective of the code is achieved.

    You should first take a look at the many delegate protocols within the built-in frameworks for an idea of what the relationship looks like when expressed in code. Here is another small example, based on your login use case. I hope you will find that the purpose of the delegation is clear and that the roles and responsibilities of the objects involved are clear and expressed through their names within the code.

    First, let's look at the LoginViewController's delegate protocol:

    #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
    

    The login controller can communicate a number of events to its delegate, as well as ask its delegate for information used to customize its behavior. It communicates events to the delegate in its implementation as part of its response to user actions:

    #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
    

    The main view controller instantiates and presents a login view controller, and handles its delegate messages:

    #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
    

    Logging in can be a complex, interactive process in some ways, so I do recommend you seriously consider using delegation instead of notifications. However, one thing that may be problematic is that delegates are necessarily only a single object. If you need to have multiple, disparate objects know about the login view controller's progress and stae, then you may need to use notifications. Especially if the login process can be constrained to be very simple, in a way that does not require any interaction beyond passing one-way messages and data, then notifications can become a viable option. You can pass arbitrary variables in a notification back inside the userInfo property which is an NSDictionary of whatever you decide to stuff in it. Notifications can impact performance but I understand that only happens nowadays when observers number in the hundreds. Even still, it is not the most natural fit in my mind, as you have the parent object (that more or less controls the lifetime of the child) asking a third party object for updates from the child object.