6

In my app delegate I create a data model and inject it into a root view controller I got from a storyboard while requesting user's credentials if needed from the start. At some point later, when accessing some data model methods, I would need to verify a user’s password and retry a request that triggered password's re-verification.

Most obvious is to build in this functionality into each view controller that may need to request this information, but I want to avoid this as much as possible because it makes controllers less generic, also makes testing harder. In my mind controllers mustn’t know anything about inner workings of the model they're given.

Adding this functionality to the model also doesn’t feels right for me: managing user interaction is all beyond responsibilities of a model in MVC.

Who should be responsible for showing a modal dialog with corresponding view controller to let a user input his credentials?

Community
  • 1
  • 1
sanmai
  • 29,083
  • 12
  • 64
  • 76
  • can't miss a funny answer: Q "Who should be responsible for showing a modal dialog with a log in form?" A: "the developer" - not? :) –  Oct 31 '13 at 17:48
  • 2
    @matheszabi uh oh, suddenly I imagine tens of thousands "developers" attached to every iPhone – sanmai Nov 01 '13 at 00:04

3 Answers3

3

It could be done using very few lines of code via callbacks. The callback API would be defined at the model layer (so it's reusable), but the user interaction is implemented at the controller level (because that's where it belongs).

I'm not entirely sure what your architecture exactly looks like, based on your description, I assume the app realizes you're not authenticated only on a failed request (you might want to store your token expiration date and leverage it, if possible).

Basic idea:

In your model, you have a callback block property (e.g. on a client class or whatever other pattern you use).

@property (nonatomic, copy) void (^onNonauthenticatedRequest)(NSURLRequest *failedRequest, NSError *error);

You execute this block in the model layer when your request fails due to the user not being authenticated.

On the controller level,you have a controller that prompts the user for credentials (and has a similar callback pattern).

client.onNonauthenticatedRequest = ^(NSURLRequest *failedRequest, NSError *error) {

    ABCredentialsViewController *credentialsViewController = [ABCredentialsViewController new];
    credentialsViewController.onAuthenticationSuccess = ^{
        // This gets called after the authentication request succeeded
        // You want to refire failedRequest here
        // Make sure you use a weak reference when using the object that owns onAuthenticationFailure
    };

    credentialsViewController.onAuthenticationFailure = ^(NSError *) {
        // You might want to do something if the user is not authenticated and failed to provide credentials
    }

    [[UIApplication sharedApplication].delegate.window.topViewController presentViewController:credentialsViewController animated:YES];
    // or you could just have a method on UIViewController/your subclass to present the credentials prompt instead
};

The logic is in the correct place and if you want to handle non-authenticated requests differently in a different case, you can.

Stepan Hruda
  • 596
  • 5
  • 12
1

It sounds to me like one of the big requirements here is that you have several controllers that may need to present the same modal dialog. To me that sounds like a delegate pattern would work well. The idea here is to keep a single set of modal dialog handling functionality that each controller can use if needed. It's also the same pattern used in UIKit internals for things like UITableViews and date pickers. https://developer.apple.com/library/ios/documentation/general/conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html offers an overview.

James
  • 1,118
  • 7
  • 13
  • Delegation is good, but if one wants everything generic, he would have to maintain delegation relationship between everyone on the scene. A model would have app delegate as its delegate. App delegate would have him as a delegate for login view controller. The last would also keep the data model as his delegate... I wonder if there is a leaner cleaner way to do this. – sanmai Nov 01 '13 at 00:02
0

You are right. Building this functionality into view controllers is unnecessary and poor encapsulation.

In the MVC paradigm, Models often have a Data Context. Data Contexts manage communication with the backend store (in iOS, this tends to be a web service or a local file) to populate and archive the Model objects. For an authenticated Data Context, you have a property for username, password and the authentication state.

@interface DataContext : NSObject
    //Authentication
    @property (nonatomic, strong) NSString * username;
    @property (nonatomic, strong) NSString * password; 
    @property (nonatomic, assign) NSInteger authenticationState;
    -(void)login;
    //Data object methods from authenticated data source (web service, etc)
    -(NSArray *)foos;
    -(NSArray *)bars;
@end

The authenticated state can be a simple boolean or an integer if you want to track many states (authenticated, unauthenticated, unauthenticated after attempting authentication with stored credentials). You can now observe the authenticationState property to allow your controller layer to take action on changes in authentication state.

When requesting data from your web service, you change the authentication state when the server refuses the request due to invalid credentials

-(NSArray *)foos
{
    NSArray * foos = nil;
    //Here you would make a web service request to populate the foos array from your web service. 
    //Here you would inspect the status code returned to capture authentication errors
    //I make my web services return status 403 unauthorized when credentials are invalid
    int statusCode = 403;
    if (statusCode == 403)
    {
         self.authenticationState = 0;//Unauthorized
    }
    return foos;
}

The Controller is your app delegate. It stores instance of our DataContext. It observes changes to that authenticated property and displays a view or reattempts authentication when appropriate.

- (void)observeAuthenticatedState:(NSNotification *)notification
{
    DataContext * context = [notification object];
    if (context.authenticatedState == 0)//You should have constants for state values if using NSIntegers. Assume 0 = unauthenticated.
    {
        [self.context login];
    }
    if (context.authenticatedState == -1)//You should have constants for state values if using NSIntegers. Assume -1 = unauthenticated after attempting authentication with stored credentials
    {
        UIViewController * loginController = nil;//Instantiate or use existing view controller to display username/password to user. 
        [[[self window] rootViewController] presentViewController:loginController
                                                         animated:YES
                                                       completion:nil];
    }
    if (context.authenticatedState == 1)//authenticated.
    {
        [[[self window] rootViewController] dismissViewControllerAnimated:YES
                                                               completion:nil];
    }
}

In your storyboard, you can basically pretend authentication doesn't exist because your app delegate interjects the user interface for authentication whenever the data context communicates it's needed.

Fruity Geek
  • 7,351
  • 1
  • 32
  • 41
  • The problem is that I don't have an authenticated state at all times. For example, user credentials may expire on the server and it may refuse a request. Then I would need refresh user's session on the server with his password and also retry the failed request. I don't see that Notifications can solve this problem while not forcing a spaghetti with meatballs type of code. – sanmai Oct 31 '13 at 14:40
  • It's not spaghetti code. The DataContext encapsulates communicating with your authenticated data source. When the server refuses the request (remember, your DataContext is the one *making* the request), you set `authenticatedState` to unauthenticated. Added request code to demonstrate. – Fruity Geek Oct 31 '13 at 17:27
  • Your code is not, but if I add to it backward notification to the calling controller, my code will very much possible turn into meatballs. Thank you very much for your input. – sanmai Oct 31 '13 at 23:58
  • I see you have updated the answer. I must take a note that we usually don't do web service request in the main thread, so we can't return the data but instead call a supplied block. – sanmai Nov 01 '13 at 04:58
  • Almost no one would do do web service requests on the main thread. I omit all the web service code because it's specific to your implementation and I assume you are competent enough to write it. I personally curry my background thread calls to inject the authentication handling. My version of `foos` doesn't return an array, but accepts a callback block to deliver the foo objects at an unspecified time in the future. – Fruity Geek Nov 01 '13 at 22:53