iOS: passing custom NSURL to NSURLProtocol
Asked Answered
T

4

6

I need to pass some extra informations along with UIWebView loadRequest: so that it reaches my implementation of NSURLProtocol. The information cannot be bound to NSURLRequest because the information must be retained with NSURLRequest mainDocumentURL as well. So i subclassed NSURL and constructed NSURLRequest with it. I already knew that the NSURLRequest which reaches NSURLProtocol startLoading is NOT the instance i have fed to UIWebView loadRequest, so i implemented NSURL copyWithZone too, naively expecting that URL loading system will use it.

Now, NSURLProtocol canInitWithRequest is called not once as one would reasonably expect, but at least 4 times before startLoading. First 2 times of that, the incoming NSURLRequest still contains my custom NSURL implementation. Then an unfortunate internal code called CFURLCopyAbsoluteURL asks for the absoluteURL of my custom NSURL and the next canInitWithRequest (and subsequent startLoading) already gets a completely new NSURLRequest with fresh NSURL in it. copyWithZone is never called and my subclassed NSURL is lost.

Before i give up and implement an inferior and fragile solution with attaching stuff directly to the URL string, i would like to ask the wizards of higher level, whether they see a way how to catch that initial blink on the NSURLProtocol radar or how to trick CFURLCopyAbsoluteURL into carrying my custom instance. I have tried to hack NSURL absoluteURL by returning again a new instance of my custom NSURL class, but it didn't help. I have seen some promise in NSURLProtocol setProperty functionality, but now it appears pretty useless. URL loading system creates new instances of everything happily and NSURLRequest arrived in NSURLProtocol seems to be the same as the one entered into UIWebView only accidentally.

UPDATE: ok i wanted to keep the post as short as possible, but the even the first reply is asking for technical background, so here we go: i've got multiple UIWebViews in app. These views may run requests concurrently and absolutely can run requests for the same URL. It's like tabs in desktop browser. But i need to distinguish which UIWebView was the origin of each particular NSURLRequest arriving to the NSURLProtocol. I need a context being carried with each URL request. I can't simply map the URLs to data, because multiple UIWebViews may be loading the same URL at any moment.

UPDATE 2: Attaching the context information to NSURL is preferred and, as far as my understanding goes, the only usable. The issue is that requests for resources referenced inside page (images etc.) do not go through UIWebViewDelegate at all and end up in NSURLProtocol directly. I don't have a chance to touch, inspect or modify such requests anywhere prior to NSURLProtocol. The only contextual link for such requests is their NSURLRequest mainDocumentURL.

Turpentine answered 30/10, 2013 at 15:35 Comment(1)
The key issue here is that you're trying to load more information on an NSURL than the URL. So the question is, what kind of information you're trying to pass through? Typically you would side-load this, by storing a map of URLs to "other data" in some object accessible to the NSURLProtocol, but I think a better understanding of what you're trying to solve would help here.Seamaid
Q
3

If there's some way to get your original NSURL used as mainDocumentURL that would be ideal. If there's no way to prevent it being copied, I thought of the following hack as an alternative:

Before the creation of each UIWebView, set the user agent string to a unique value. Supposedly this change only affects UIWebView objects that are created subsequently, so each view will end up with its own distinctive user agent string.

In the NSURLProtocol implementation, you can check the user agent string to identify the associated UIWebView and pass it through to the real protocol handler using the actual user agent string (so the server will see nothing different).

All this depends on the views really ending up with different UA strings. Let me know if you manage to get it to work!

Quickly answered 31/10, 2013 at 7:50 Comment(2)
This works for me, despite depending on at least two undocumented iOS features. Changing User-Agent really affects only the newly created UIWebViews, not the existing ones. For the record, you need to pass at least one NSURLRequest through the newly created UIWebView to make the User-Agent "stick".Turpentine
Pavel, how much data you need to pass through NSURL? If you only need "to distinguish which UIWebView was the origin of each particular NSURLRequest arriving to the NSURLProtocol" you can use setCachePolicy with your own value for your main request. And NSURLProtocol will pass that value for child requests of UIWebView. Anyway you can set cachePolicy to default when you catch it. But I'm not shure that is a good way... What you think?Jeraldjeraldine
S
1

You say that you can't put it on the NSURLRequest, but I'm not clear why from your updated discussion. That would be the most natural place to put it.

  • Implement webView:shouldLoadWithRequest:navigationType:.
  • Attach an extra property to the provided request using objc_setAssociatedObject. Then return YES. (It would be nice to use setProperty:forKey:inRequest: here, but UIWebView passes us a non-mutable request, so we can only attach associated objects. Yet another way that UIWebView is a pale shadow of OS X's WebView, which can handle this).
  • In the NSProtocol, read your extra property using objc_getAssociatedObject. The request should be the same one you were presented earlier. You suggest that this isn't the case. Are you saying that the request at webView:shouldLoadWithRequest:navigationType: is different than the request at initWithRequest:cachedResponse:client:?

Am I missing another requirement or quirk?

Seamaid answered 30/10, 2013 at 20:49 Comment(1)
For the preference of NSURL over NSURLRequest see UPDATE2. I was almost going for objc_setAssociatedObject in the very beginning of this pilgrimage, but then i found out that, and i confirm it, that NSURLProtocol is consulted with the original object only initially, then handed with completely new instances of both NSURLRequest and the enclosed NSURL.Turpentine
G
1

You can pass options through custom request headers, assuming that the targeted website or service provider don't somehow strip those in transit.

The challenge there would be coming up with an encoding scheme that can be reasonable encoded into an ASCII string for the header field value and then decoded into the actual value you want. For this, a custom NSValueTransformer would seem most appropriate.

Greenway answered 11/11, 2013 at 18:22 Comment(1)
Thanks for picking up my email call and sorry for the late reaction. I was off to something else. I ended up using request header, so +1, but not the way you're suggesting. The problem with your solution is that i'm not creating the requests myself, they're sourced in UIWebView. As you probably know, the interface mockery called UIWebViewDelegate consults just the frame URLs with you. I need to distinguish all resource requests.Turpentine
C
0

I had the same problem. I finally stuck to the solution suggested by Matthew (using the user agent string). However, as the solution is not fleshed out, I add a new answer with more details. Furthermore, I found out that you do not need to send a request to make the user agent "stick". It is sufficient to get via javascript as suggested here.

Following steps worked for me:

(1) Get the current default user agent. You need it later to put it back into the request in NSURLProtocol. You need to use a new webview insatnce, as getting the user agent will make it stick to the webview, so you can not change it later on.

UIWebView* myWebview = [[UIWebView alloc] init];
NSString* defaultUserAgent = [myWebview stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
[myWebview release]; // no needed with ARC, but to emphasize, that the webview instance is not needed anymore

(2) Change the value in the standardUserDefaults (taken from here).

NSDictionary* userAgentDict = [NSDictionary dictionaryWithObjectsAndKeys:@"yourUserAgent", @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:userAgentDict];

(3) Make the new user agent string stick to your webView by getting it via javascript as done in (1), but this time on the webview instance you actually work with.

(4) Revert the default user agent in the standardUserDefaults as done here.

Cranage answered 25/7, 2014 at 14:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.