Итак, у нас имеется внешняя веб страница проекта-партнера, которая содержит веб-форму. Страница отлично работает во встроенном в приложение браузере, но ее внешний вид не совпадает с представлениями о прекрасном нашего отдела дизайна и выглядит внутри неорганично. Дизайнеры рисуют новую красивую форму и дают команду: «Должно выглядеть так!». У всех свои задачи, но наша общая цель – качественное приложение.
Наша задача ясна. Приступаем к реализации. Внедрить форму в приложение в новом дизайне — ничего сложного. Но как быть с веб-формой?
Навскидку, можно реализовать программно логику работы страницы с формой. Потом сформировать 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/
Добавить комментарий