When developing a mobile application for a project that has to work with a large number of external systems, situations inevitably arise in which it is necessary to show ingenuity and ingenuity. Especially often such situations arise when trying to implement a programmed flight of thoughts of a designer, taking into account the technical features of such systems. How we solve such problems when working on the Money.Ru.Money mobile application, we will discuss in this article.


So, we have an external partner project web page that contains a web form. The page works fine in the browser built into the application, but its appearance does not coincide with the ideas about the beauty of our design department and looks inorganically inward. Designers draw a new beautiful shape and give the command: "It should look like this!". Everyone has their own tasks, but our common goal is a quality application.
Our task is clear. We start implementation. Embed the form in the application in a new design - nothing complicated. But what about the web form?
')
Offhand, you can programmatically implement the logic of the page with the form. Then create an HTTP request that emulates pressing the “Send” button and transmit it to
UIWebView .
However, with all the simplicity of this approach, there are pitfalls. A form can easily contain a
CSRF token (then we will have to load the page and parse the token in order to transfer it in the final request), a list of values ​​that can change frequently on the server side (also load and parse), and generally manipulate the state one or more hidden form fields (hello, javascript!) depending on the data entered by the user. All this rather complicates the task, don't you find?
There is another way! And on the stage, under the cheers of the audience, Maestro Crutch appears. What are we doing?
Everything is very simple. We take the
UIWebView hidden from the user's eyes, load our web page there and manipulate with its DOM objects using JavaScript.
Consider this technique for a simple example. As a guinea pig, take the search form in the upper right corner of the main Habr page, which has the following HTML representation:
<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>
The form is simple and contains only one text input field and a button, therefore it is an ideal object for an experiment.
First of all, create a controller that will manage the web form.
@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
Our controller contains a
UIWebView , into which we will load the page with the form, and an
NSURLRequest object, which we will use to store the request to load the page. Specifying the
autoresizingMask property for the view object will allow you to use this controller as a child view controller without further problems, and the
alpha property will control its visibility.
Create somewhere in the depths of our project a controller object and load the page with the form into it.
static NSString *kMRHabraURLString = @"http://habrahabr.ru"; MRWebViewController *controller = [[MRWebViewController alloc] initWithURLString:kMRHabraURLString]; [controller reload];
At the same time, the result of loading the page is intercepted by the corresponding delegate function in our controller. It is convenient to manipulate DOM objects using jQuery. Therefore, we make sure that it will definitely be present in the loaded page.
- (void)webViewDidFinishLoad:(UIWebView *)webView { if (!self.hasForm) { NSLog(@"Installing jQuery at %@", webView.request.URL.absoluteString); [self.webView stringByEvaluatingJavaScriptFromString:[MRScriptsFactory jqueryScript]]; self.hasForm = YES; } // ... }
The process of loading a web page is asynchronous, and although the page may still not fully load, nothing prevents us from displaying the native form programmatically for the user at this moment. In this case, the native form assumes responsibility for the input and verification of data received from the user.
After the user has filled out the native form and clicked the “Search” button in it, our controller receives the message
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; }
In our case, the script obtained through
[MRScriptsFactory fillFormScript] has the form:
(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, '%@');
As can be seen from the source code of the script, it fills the text field of the form with a search string and programmatically emulates pressing the form button.
As we didn’t initially provide for any further processing of the data received as a result of the execution of the request in UIWebView, in our example we simply “manifest” it to the user.
- (void)webViewDidFinishLoad:(UIWebView *)webView { if (!self.hasForm) { // ... } else if (self.isScriptExecuting) { [UIView animateWithDuration:0.3 animations:^{ self.view.alpha = 1.0; }]; self.scriptExecuting = NO; // ... } }
This approach has been successfully used by us for a long time and has proven itself well. The full source code of the example is located
here.If you have questions, or you want to share your best practices on working with forms, I suggest discussing this in the comments.