С любовью к дизайнерам: внедряем веб-формы в мобильное приложение

от автора

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


     
Итак, у нас имеется внешняя веб страница проекта-партнера, которая содержит веб-форму. Страница отлично работает во встроенном в приложение браузере, но ее внешний вид не совпадает с представлениями о прекрасном нашего отдела дизайна и выглядит внутри неорганично. Дизайнеры рисуют новую красивую форму и дают команду: «Должно выглядеть так!». У всех свои задачи, но наша общая цель – качественное приложение.

Наша задача ясна. Приступаем к реализации. Внедрить форму в приложение в новом дизайне — ничего сложного. Но как быть с веб-формой?

Навскидку, можно реализовать программно логику работы страницы с формой. Потом сформировать HTTP-запрос, эмулирующий нажатие кнопки «Отправить», и передать его в UIWebView

Однако, при всей простоте у такого подхода есть подводные камни. Форма запросто может содержать в себе CSRF-токен (тогда нам придется загружать страницу и парсить токен, чтобы передать его в итоговом запросе), список выбора значений, которые могут часто меняться на стороне сервера (тоже загружать и парсить), да и вообще манипулировать состоянием одного или нескольких скрытых полей формы (привет, JavaScript!) в зависимости от данных, введенных пользователем. Все это достаточно усложняет задачу, не находите?
 
Есть другой путь! И на сцене под овации зрителей появляется маэстро Костыль. Что мы делаем?

Все очень просто. Берем скрытый от глаз пользователя UIWebView, загружаем туда нашу веб-страницу и манипулируем с ее объектами DOM при помощи JavaScript.

Рассмотрим данную технику на простом примере. В качестве подопытного кролика возьмем форму поиска в правом верхнем углу главной страницы Хабра, которая имеет следующее HTML-представление:
 

<div class="search">   <form id="search_form" name="search" method="get" action="//habrahabr.ru/search/">     <input type="submit" value="">     <input type="text" name="q" x-webkit-speech="" speech="" tabindex="1" autocomplete="off">   </form> </div>

Форма проста и содержит в себе только одно текстовое поле ввода и кнопку, поэтому является идеальным объектом для эксперимента.
Первым делом создаем контроллер, который будет управлять веб-формой.

@interface MRWebViewController () <UIWebViewDelegate> @property (nonatomic, weak, readonly) UIWebView *webView; @property (nonatomic, strong, readonly) NSURLRequest *request; @property (nonatomic, assign) BOOL hasForm;   // ...   @end   @implementation MRWebViewController { }   // ... - (instancetype)initWithURLString:(NSString *)urlString {     self = [super init];     if (self) {         _request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];     }     return self; }   - (void)viewDidLoad {     [super viewDidLoad];     [self createWebView];     self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;     self.view.alpha = 0.0; }   - (void)createWebView {     UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];     webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;     webView.backgroundColor = UIColor.whiteColor;     webView.scalesPageToFit = YES;     webView.delegate = self;     [self.view addSubview:webView];     _webView = webView; }   // ...   - (void)reload {     self.hasForm = NO;     self.view.alpha = 0.0;     [self.webView stopLoading];     [self.webView loadRequest:self.request]; }   // ...   @end

Наш контроллер содержит UIWebView, в который мы будем загружать страницу с формой, и объект NSURLRequest, который мы будем использовать для хранения запроса для загрузки страницы. Указание свойства autoresizingMask для объекта view позволит в дальнейшем без проблем использовать данный контроллер в качестве child view controller, а свойством alpha будем управлять его видимостью.

Создадим где-то в недрах нашего проекта объект контроллера и загрузим в него страницу с формой.

static NSString *kMRHabraURLString = @"http://habrahabr.ru";   MRWebViewController *controller = [[MRWebViewController alloc] initWithURLString:kMRHabraURLString];  [controller reload]; 

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

- (void)webViewDidFinishLoad:(UIWebView *)webView {     if (!self.hasForm) {         NSLog(@"Installing jQuery at %@", webView.request.URL.absoluteString);         [self.webView stringByEvaluatingJavaScriptFromString:[MRScriptsFactory jqueryScript]];         self.hasForm = YES;     } // ... }

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

После того, как пользователь заполнил нативную форму и нажал в ней на кнопку «Искать», наш контроллер получает сообщение searchWithString:.

- (BOOL)searchWithString:(NSString *)searchString {     BOOL result = NO;     if (self.hasForm) {         // ...         NSString *actualString = [searchString stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"];         NSString *script = [NSString stringWithFormat:[MRScriptsFactory fillFormScript], actualString];         NSString *scriptResult = [self.webView stringByEvaluatingJavaScriptFromString:script];         __autoreleasing NSError *error = nil;         id object = [NSJSONSerialization JSONObjectWithData:[scriptResult dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&error];         result = (!error && [object isKindOfClass:[NSDictionary class]] && [object[@"success"] boolValue]);                  // ...     }     return result; }

В нашем случае скрипт, получаемый через [MRScriptsFactory fillFormScript], имеет вид:

 (function ($, searchString) {     var components = {         $text : $("form#search_form input[type='text']"),         $submit : $("form#search_form input[type='submit']")     };     components.$text.val(searchString);     components.$submit.click();     return JSON.stringify({         "success" : true     }); })(jQuery, '%@');

Как видно из исходного кода скрипта, он производит заполнение текстового поля формы строкой поиска и программно эмулирует нажатие на кнопку формы.

Так как никакой последующей обработки данных, получаемых в результате исполнения запроса в UIWebView, нами изначально не предусматривалось, то в нашем примере мы просто «проявляем» его пользователю.

- (void)webViewDidFinishLoad:(UIWebView *)webView {     if (!self.hasForm) {        // ...     } else if (self.isScriptExecuting) {         [UIView animateWithDuration:0.3 animations:^{             self.view.alpha = 1.0;         }];         self.scriptExecuting = NO;         // ...     } }

Данный подход успешно применяется нами длительное время и хорошо себя зарекомендовал. Полный исходный код примера располагается здесь

Если у вас есть вопросы, или вы хотите поделиться своими best practices по работе с формами, предлагаю обсудить это в комментариях.

ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/211522/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *