Кто должен нести ответственность за отображение модального диалога с формой входа в систему?

В моем делегате приложения я создаю модель данных и вставляю ее в контроллер корневого представления, который я получил из раскадровки при запросе учетных данных пользователя, если это необходимо с самого начала. В какой-то момент позже, при доступе к некоторым методам модели данных, мне нужно было проверить пароль пользователя и повторить запрос, который вызвал повторную проверку пароля.

Наиболее очевидным является встраивание этой функциональности в каждый контроллер представления, которому может понадобиться запрашивать эту информацию, но я хочу избежать этого, насколько это возможно, потому что это делает контроллеры менее универсальными, а также усложняет тестирование. На мой взгляд, контролеры не должны ничего знать о внутренней работе модели, которую им дают.

Добавление этой функциональности в модель также кажется мне неправильным: управление взаимодействием с пользователем выходит за рамки обязанностей модели в MVC.

Кто должен нести ответственность за отображение модального диалога с соответствующим контроллером представления, чтобы пользователь мог ввести свои учетные данные?


person sanmai    schedule 30.10.2013    source источник
comment
не могу пропустить забавный ответ: Q Кто должен отвечать за отображение модального диалога с формой входа? О: разработчик - нет? :)   -  person    schedule 31.10.2013
comment
@matheszabi ох, внезапно я представляю себе десятки тысяч разработчиков, прикрепленных к каждому iPhone   -  person sanmai    schedule 01.11.2013


Ответы (3)


Это можно сделать, используя очень мало строк кода с помощью обратных вызовов. API обратного вызова будет определен на уровне модели (поэтому его можно будет использовать повторно), но взаимодействие с пользователем реализовано на уровне контроллера (потому что именно там оно и находится).

Я не совсем уверен, как именно выглядит ваша архитектура, основываясь на вашем описании, я предполагаю, что приложение понимает, что вы не аутентифицированы только при неудачном запросе (вы можете сохранить дату истечения срока действия вашего токена и использовать его, если это возможно) .

Основная идея:

В вашей модели у вас есть свойство блока обратного вызова (например, в клиентском классе или любом другом шаблоне, который вы используете).

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

Вы выполняете этот блок на уровне модели, когда ваш запрос завершается ошибкой из-за того, что пользователь не прошел аутентификацию.

На уровне контроллера у вас есть контроллер, который запрашивает у пользователя учетные данные (и имеет аналогичный шаблон обратного вызова).

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

Логика находится в правильном месте, и если вы хотите по-разному обрабатывать неаутентифицированные запросы в другом случае, вы можете это сделать.

person Stepan Hruda    schedule 05.11.2013
comment
Выглядит хорошо для меня. Думал, что есть много серых мест, но ты не должен делать за меня домашнюю работу, верно? Спасибо! - person sanmai; 08.11.2013

Мне кажется, что одним из основных требований здесь является наличие нескольких контроллеров, которым может потребоваться представление одного и того же модального диалога. Для меня это звучит так, как если бы шаблон делегата работал хорошо. Идея состоит в том, чтобы сохранить единый набор функций обработки модальных диалогов, которые каждый контроллер может использовать при необходимости. Это также тот же шаблон, который используется во внутренних компонентах UIKit для таких вещей, как UITableViews и средства выбора даты. https://developer.apple.com/library/ios/documentation/general/conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html предлагает обзор.

person James    schedule 31.10.2013
comment
Делегирование — это хорошо, но если кто-то хочет, чтобы все было общим, ему придется поддерживать отношения делегирования между всеми на сцене. Модель будет иметь делегата приложения в качестве своего делегата. Делегат приложения будет иметь его в качестве делегата для контроллера входа в систему. Последний также сохранит модель данных в качестве своего делегата... Интересно, есть ли более компактный и чистый способ сделать это. - person sanmai; 01.11.2013

Ты прав. Встраивание этой функциональности в контроллеры представлений является ненужным и плохой инкапсуляцией.

В парадигме MVC модели часто имеют контекст данных. Контексты данных управляют связью с внутренним хранилищем (в iOS это, как правило, веб-служба или локальный файл) для заполнения и архивирования объектов модели. Для аутентифицированного контекста данных у вас есть свойство для имени пользователя, пароля и состояния аутентификации.

@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

Аутентифицированное состояние может быть простым логическим значением или целым числом, если вы хотите отслеживать множество состояний (аутентифицировано, неаутентифицировано, неаутентифицировано после попытки аутентификации с сохраненными учетными данными). Теперь вы можете наблюдать за свойством authenticationState, чтобы позволить вашему уровню контроллера принимать меры при изменении состояния аутентификации.

При запросе данных от вашего веб-сервиса вы меняете состояние аутентификации, когда сервер отклоняет запрос из-за неверных учетных данных.

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

Контроллер — это делегат вашего приложения. В нем хранится экземпляр нашего DataContext. Он отслеживает изменения в этом свойстве authenticated и отображает представление или повторяет попытку аутентификации, когда это необходимо.

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

В вашей раскадровке вы можете в основном притворяться, что аутентификация не существует, потому что ваш делегат приложения вставляет пользовательский интерфейс для аутентификации всякий раз, когда контекст данных сообщает, что это необходимо.

person Fruity Geek    schedule 30.10.2013
comment
Проблема в том, что у меня нет постоянного состояния аутентификации. Например, срок действия учетных данных пользователя на сервере может истечь, и он может отклонить запрос. Затем мне нужно будет обновить сеанс пользователя на сервере с его паролем, а также повторить неудачный запрос. Я не вижу, что уведомления могут решить эту проблему, не заставляя код типа спагетти с фрикадельками. - person sanmai; 31.10.2013
comment
Это не спагетти-код. DataContext инкапсулирует обмен данными с вашим аутентифицированным источником данных. Когда сервер отклоняет запрос (помните, что ваш DataContext тот, кто отправляет запрос), вы устанавливаете authenticatedState как неаутентифицированный. Добавлен код запроса для демонстрации. - person Fruity Geek; 31.10.2013
comment
Ваш код — нет, но если я добавлю к нему обратное уведомление вызывающему контроллеру, мой код вполне может превратиться в фрикадельки. Большое спасибо за ваш вклад. - person sanmai; 01.11.2013
comment
Я вижу, вы обновили ответ. Я должен принять к сведению, что обычно мы не делаем запрос веб-службы в основном потоке, поэтому мы не можем вернуть данные, а вместо этого вызываем предоставленный блок. - person sanmai; 01.11.2013
comment
Почти никто не стал бы делать запросы к веб-службе в основном потоке. Я опускаю весь код веб-службы, потому что он специфичен для вашей реализации, и я предполагаю, что вы достаточно компетентны, чтобы написать его. Я лично выполняю вызовы фонового потока для внедрения обработки аутентификации. Моя версия foos не возвращает массив, но принимает блок обратного вызова для доставки объектов foo в неопределенное время в будущем. - person Fruity Geek; 02.11.2013