Customizing UIWebView requests with NSURLProtocol

Whether you are creating your own web browser on iOS or just displaying some custom, locally generated HTML,UIWebView can become a source of lots of frustration, mostly related to its (perceived) lack of basic customization options.

Today, we are going to open the pandora’s box and dive into theFoundation’s URL Loading System to customize every requestUIWebView sends.

UIWebView uses the NSURLConnection class for every request (actually, the NSURLConnection class was originally written for the first release of Safari, that’s why they are so tied together). And every NSURLConnection request is intercepted and treated accordingly either by the cache or other custom protocol handlers. By creating a custom protocol handler (using NSURLProtocol), we can intercept, match and customize every request sent by UIWebView.

For today’s example, let’s say that we want our UIWebView on iOS to be seen as a Chrome Desktop browser running on Windows. According toUserAgentString.com the current Chrome user agent is:

Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36

So let’s start by creating a subclass of NSURLProtocol:

#import <Foundation/Foundation.h>
 
@interface ChromeBrowserURLProtocol : NSURLProtocol
 
@end
 
@interface ChromeBrowserURLProtocol ()
@property (nonatomic, strong) NSURLConnection *connection;
 
@end
 
@implementation ChromeBrowserURLProtocol
 
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
   if ([NSURLProtocol propertyForKey:@“UserAgentSet” inRequest:request] != nil)
      return NO;
 
   return YES;
}

Whenever the URL Loading system receives a new request, it queries every available protocol handler to determine who can handle that request. Here we use a helper method from NSURLProtocol, called+propertyForKey:inRequest:. This method queries for a custom property that can be applied on any NSMutableURLRequest. On our example, we look for “UserAgentSet”. If the user agent has been set, pass this along. Otherwise, let’s deal with this.

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
   return request;
}
 
- (void)startLoading
{
   NSMutableURLRequest *newRequest = [self.request mutableCopy];
 
   // Here we set the User Agent
   [newRequest setValue:@"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36 Kifi/1.0f" forHTTPHeaderField:@"User-Agent"];
 
   [NSURLProtocol setProperty:@YES forKey:@"UserAgentSet" inRequest:newRequest];
 
   self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
}

On -startLoading, we create a mutableCopy of our request and change the User Agent. We must set the “UserAgentSet” property here with +setProperty:forKey:inRequest:, so we know to pass this request along in the future. After that, we just initiate a new NSURLConnection with the request.

- (void)stopLoading
{
   [self.connection cancel];
}
 
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
   [self.client URLProtocol:self didLoadData:data];
}
 
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
   [self.client URLProtocol:self didFailWithError:error];
   self.connection = nil;
}
 
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
   [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
}
 
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
   [self.client URLProtocolDidFinishLoading:self];
   self.connection = nil;
}
 
@end

These are the delegates for the NSURLConnection class, and we must pass them along to our inner client object, contained inside NSURLProtocol.

Then, we must register this protocol on you App Delegate’s -(BOOL)application:didFinishLaunchingWithOptions:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [NSURLProtocol registerClass:[ChromeBrowserURLProtocol class]];
...

And that’s it!

Of course this only scratches the surface on what can be done with NSURLProtocol. You can create your own caching mechanism, track redirects (with -connection:willSendRequest:redirectResponse:response:), sign requests, mock HTTP responses for testing and a lot more.

转自:http://eng.kifi.com/customizing-uiwebview-requests-with-nsurlprotocol/

 

posted @ 2015-06-23 16:06  mumoozhu  阅读(617)  评论(0编辑  收藏  举报