При разработке мобильного приложения для проекта, которому приходится работать с большим количеством внешних систем, неизбежно возникают ситуации, в которых приходится проявлять находчивость и смекалку. Особенно часто такие ситуации возникают при попытках реализовать программно полет мысли дизайнера с учетом технических особенностей таких систем. О том, как мы решаем такие задачи при работе над мобильным приложением Денег 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 по работе с формами, предлагаю обсудить это в комментариях.
Автор: mpain1978