UIWebViewDelegate not monitoring XMLHttpRequest?
Asked Answered
P

5

54

Is it true that the UIWebViewDelegate does not monitor requests made by using a XMLHttpRequest? If so, is there a way to monitor these kind of requests?

e.g. UIWebViewDelegate does not catch this in -(BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSMutableURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

var xhr = new XMLHttpRequest();
xhr.open("GET", "http://www.google.com", true);

xhr.onreadystatechange=function() 
{
    if (xhr.readyState==4) 
    {
        alert(xhr.responseText);
    }
}

xhr.send();
Physicalism answered 18/3, 2011 at 14:18 Comment(0)
M
80

Interesting question.

There are two parts to make this work: a JavaScript handler and UIWebView delegate methods. In JavaScript, we can modify prototype methods to trigger events when an AJAX request is created. With our UIWebView delegate, we can capture these events.

JavaScript Handler

We need to be notified when an AJAX request is made. I found the solution here.

In our case, to make the code work, I put the following JavaScript in a resource called ajax_handler.js which is bundled with my app.

var s_ajaxListener = new Object();
s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open;
s_ajaxListener.tempSend = XMLHttpRequest.prototype.send;
s_ajaxListener.callback = function () {
    window.location='mpAjaxHandler://' + this.url;
};

XMLHttpRequest.prototype.open = function(a,b) {
  if (!a) var a='';
  if (!b) var b='';
  s_ajaxListener.tempOpen.apply(this, arguments);
  s_ajaxListener.method = a;  
  s_ajaxListener.url = b;
  if (a.toLowerCase() == 'get') {
    s_ajaxListener.data = b.split('?');
    s_ajaxListener.data = s_ajaxListener.data[1];
  }
}

XMLHttpRequest.prototype.send = function(a,b) {
  if (!a) var a='';
  if (!b) var b='';
  s_ajaxListener.tempSend.apply(this, arguments);
  if(s_ajaxListener.method.toLowerCase() == 'post')s_ajaxListener.data = a;
  s_ajaxListener.callback();
}

What this will actually do is change the location of the browser to some made up URL scheme (in this case, mpAjaxHandle) with info about the request made. Don't worry, our delegate with catch this and the location won't change.

UIWebView Delegate

First, we need to read our JavaScript file. I suggest doing storing it in a static variable. I'm in the habit of using +initialize.

static NSString *JSHandler;

+ (void)initialize {
    JSHandler = [[NSString stringWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"ajax_handler" withExtension:@"js"] encoding:NSUTF8StringEncoding error:nil] retain];
}

Next, we want to inject this JavaScript before a page is done loading so we can receive all events.

- (void)webViewDidStartLoad:(UIWebView *)webView {
    [webView stringByEvaluatingJavaScriptFromString:JSHandler];
}

Finally, we want to capture the event.

Since the URL Scheme is made up, we don't want to actually follow it. We return NO and all is well.

#define CocoaJSHandler          @"mpAjaxHandler"

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if ([[[request URL] scheme] isEqual:CocoaJSHandler]) {
        NSString *requestedURLString = [[[request URL] absoluteString] substringFromIndex:[CocoaJSHandler length] + 3];

        NSLog(@"ajax request: %@", requestedURLString);
        return NO;
    }

    return YES;
}

I created a sample project with the solution but have nowhere to host it. You can message me if you can host it and I'll edit this post accordingly.

Maxma answered 21/4, 2011 at 2:3 Comment(14)
although this doesn't allow editing the request, that was not my question. So thanks! :)Physicalism
Take care that XCode doesn't automatically add files with the extensions .js to the bundle. You need to add the file manually.Disenthral
Hi Mike - Any Chance you could share the sample project with me? I would really appreciate itMatzo
You helped me so much, this was a huge issue for me. Now everything works perfectly :) :) Also thanks for your super detailed explanation, you're the man! ;)Annecorinne
This question is related: #12563811Benzaldehyde
If you have trouble getting this working it may be because Xcode is trying to compile your JS code. More info about this at https://mcmap.net/q/145525/-uiwebview-doesn-39-t-load-external-javascript-fileQuinine
The javascript stops working after returning from another site (in my case, for third party authentication) and even after re-injecting the js. It works when injecting in webViewDidFinishLoad.Biennial
This question is old but if you still have the code you should post it on GitHubSupat
Note I just whacked the JS sample straight into an NSString to test and it didn't work at first. I had to add a ; after the trailing } of the XMLHttpRequest.prototype.send/open assignment.Ylangylang
Note also I added an if(!s_ajaxListener) around the whole lot it continued to work even if the JS was run in/evaluated twice.Ylangylang
I'm not sure why handler is injected at webViewDidStartLoad, shouldn't it be at webViewDidFinishLoad, after page is loaded?Thilde
So I have a request coming through there the subject and body are being set like so: xmlhttp.send({ subject:"This is the subject", body:JSON.stringify(jsonString)}); This get's assigned here: if(s_ajaxListener.method.toLowerCase() == 'post')s_ajaxListener.data = a; But I can't seem to figure out how to access this data property in the request passed in. Any ideas?Unashamed
I need to extend this in 2 ways: 1) Add a custom header with a dynamic value (which Swift knows, but the static JS wouldn't) to every request and 2) Alter the URL's path to add a static prefix to every request before issuing it. Does anyone know how to do that?Nonmetallic
The solution gave me some issues with animation. It seems like the window.location change caused animations to stop. Luckily, you can do a trick very similar to this: have an iframe do the load. It still trigger's the delegate's methods, and it doesn't stop the parent page's animation. Also, I think this project implemented it with an iframe, might be useful to some. github.com/tcoulter/jockeyjsWotton
R
20

You can use an NSURLProtocol. For instance if you call XMLHttpRequest with http://localhost/path you can handle it with the following:

@interface YourProtocol: NSURLProtocol

Then for the implementation:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request 
{
    return [request.URL.host isEqualToString:@"localhost"];
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

- (void) startLoading
{
    // Here you handle self.request 
}

- (void)stopLoading
{
}

You need to register the protocol as follows:

    [NSURLProtocol registerClass:[YourProtocol class]];
Ray answered 24/7, 2012 at 4:56 Comment(4)
The problem here is that you lose all and any reference to your UIWebView unless you keep up a list, and even then, that is not 100% reliablePhysicalism
actually this is a much healtier sollution compared with the other one (which is very hackish). You can make a class reference somewhere to your UIWebView instance if you need to... This method is also much more capable (if you wanted more than just monitoring)Fergus
It is worth mentioning that you would need to call [self.client URLProtocolDidFinishLoading:self]; in startLoading for the request to complete. (Otherwise it will just timeout in the javascript end.)Shovelnose
More correctly you would need to call something like [self.client URLProtocol:self didReceiveResponse:[NSURLResponse new] cacheStoragePolicy:NSURLCacheStorageNotAllowed]; then [self.client URLProtocolDidFinishLoading:self]; in startLoading for the request to complete.Shovelnose
L
3

By implementing and registering a subclass of NSURLProtocol you can capture all the request from your UIWebView. It may be overkill in some cases but if you are not able to modify the javascript actually being run it is your best bet.

In my case I need to capture all the request and insert a specific HTTP header to every one of them. I have done this by implementing NSURLProtocol, registering it using registerClass and answering YES in my subclass to + (BOOL)canInitWithRequest:(NSURLRequest *)request if the request corresponds to the URLs I am interested in. You then have to implement the other methods of the protocol this can be done by using an NSURLConnection, setting the protocol class as the delegate and redirecting the delegate methods of NSURLConnection to NSURLProtocolClient

Labannah answered 31/1, 2012 at 9:53 Comment(1)
Specifically if what you want is just to monitor the request, (without modifying it). This is very easy: 1) create a subclass of NSURLProtocol 2) implement + (BOOL)canInitWithRequest:(NSURLRequest *)request { return NO;} 3) register your protocol using [NSURLProtocol registerClass:lShareSwymURLProtocol]; in the implementation in step 2) you can add whatever code you want after inspecting the request. you just make sure to return NO if you want the standard load mechanism to continueLabannah
P
1

It does appear to be true. There is no way to monitor what a UIWebView is doing beyond what UIWebViewDelegate provides, unless perhaps you can figure out a way to use stringByEvaluatingJavaScriptFromString: to inject some Javascript to do what you need.

Peay answered 14/4, 2011 at 18:59 Comment(0)
B
0

As other peoples mentioned, UIWebViewDelegate observes changes of window.location and iframe.src only.

In case that you just want to use custom url scheme of iOS only, you can take advantage of <iframe> like this way.

For example, if you want to call a URL Scheme like this

objc://my_action?arg1=1&arg2=2

in your js file:

/**
* @param msg : path of your query
* @param data : arguments list of your query
*/
var call_url = function(msg, data){
    const scheme = "objc";
    var url = scheme + '://' + msg;
    if(data){
        var pstr = [];
        for(var k in data)
            if(typeof data[k] != 'function')
                pstr.push(encodeURIComponent(k)+"="+encodeURIComponent(data[k]));
        url += '?'+pstr.join('&');
    }
    var i = document.createElement("iframe");
    i.src = url;
    i.style.opacity=0;
    document.body.appendChild(i);
    setTimeout(function(){i.parentNode.removeChild(i)},200);
}

//when you call this custom url scheme
call_url ("my_action", {arg1:1,arg2:2});
Bumper answered 19/2, 2016 at 5:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.