How can I change the user-agent of UIWebView in iOS 5?
What I have done so far:
Using the delegate call back, intercept the NSURLRequest, create a new url request and set it's user-agent as whatever I want, then download the data and reload the UIWebView with "loadData:MIMEType:....".
Problem:
This causes infinite recursion, where I load the data, which calls the delegate back, which intern calls the delegate....
Here's the delegate method:
- (BOOL)webView:(UIWebView *)aWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
dispatch_async(kBgQueue, ^{
NSURLResponse *response = nil;
NSMutableURLRequest *newRequest = [NSMutableURLRequest requestWithURL:[request URL]];
NSDictionary *headers = [NSDictionary dictionaryWithObject:
#"custom_test_agent" forKey:#"User-Agent"];
[newRequest setAllHTTPHeaderFields:headers];
[self setCurrentReqest:newRequest];
NSData *data = [NSURLConnection sendSynchronousRequest:newRequest
returningResponse:&response
error:nil];
dispatch_sync(dispatch_get_main_queue(), ^{
[webView loadData:data
MIMEType:[response MIMEType]
textEncodingName:[response textEncodingName]
baseURL:[request URL]];
});
});
return YES;
}
Change the "UserAgent" default value by running this code once when your app starts:
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:#"Your user agent", #"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
EDIT: I have used this with great success, but want to add additional details. To get a user agent, you can enable the "Developer" menu, set the user agent, and then connect to this site to get it printed out for you: WhatsMyAgent. Likewise you can connect using any kind of mobile device, and get it that way too. BTW this is still working just fine in iOS7+
In Swift use this to set UserAgent,
func setUserAgent(){
var userAgent = NSDictionary(objectsAndKeys: "YourUserAgentName","UserAgent")
NSUserDefaults.standardUserDefaults().registerDefaults(userAgent as [NSObject : AnyObject])
}
Use this to test,
println(WebView.stringByEvaluatingJavaScriptFromString("navigator.userAgent"));
When you send message [aWebView loadData:MIMEType:textEncodingName:baseURL:]
then aWebView shouldStartLoadWithRequest: will be called again, and then again - that is why you get an infinite recursion
You should restrict calling of your dispatch_async() block, for example by using some conventional URL:
- (BOOL)webView:(UIWebView *)aWebView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
if ([[[request URL] absoluteString] isEqualToString:#"http://yourdomain.com/?local=true"]) {
return YES;
}
...
dispatch_async(...
[aWebView loadData:data
MIMEType:[response MIMEType]
textEncodingName:[response textEncodingName]
baseURL:[NSURL URLWithString:#"http://yourdomain.com/?local=true"]];
);
return NO;
}
Related
I'm having an issue with a login API. First call works fine, but subsequent calls are cached. This is causing an issue since login/logout functionality is essentially broke.
I've tried many methods and I'm implementing AFNetworking library.
In AppDelegate.m:
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0
diskCapacity:0
diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
In my Networking class:
(AFHTTPRequestOperation *)createRequestOperationWithMethod:(NSString *) method andPath: (NSString *)path andParams:(NSDictionary *)params
{
GRAPIClient *httpClient = [GRAPIClient sharedClient];
[httpClient setParameterEncoding:AFFormURLParameterEncoding];
NSMutableURLRequest *request = [httpClient requestWithMethod:method
path:path
parameters:params];
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData]
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
return operation;
}
I even tried to overwrite the request being generated in AFHTTPClient
In AFHTTPClient.m:
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
[request setTimeoutInterval:2.0];
My GRAPIClient implementation:
#interface GRAPIClient : AFHTTPClient
+ (GRAPIClient *)sharedClient;
+ (BOOL) isInternetReachable;
#end
#implementation GRAPIClient
+ (BOOL) isInternetReachable
{
return reachable;
}
+ (GRAPIClient *)sharedClient {
static GRAPIClient *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[GRAPIClient alloc] initWithBaseURL:[NSURL URLWithString:kAFAppDotNetAPIBaseURLString]];
});
[_sharedClient setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
if (status == AFNetworkReachabilityStatusReachableViaWWAN ||
status == AFNetworkReachabilityStatusReachableViaWiFi ) {
NSLog(#"Reachable on!");
reachable = YES;
}
else
{
NSLog(#"Reachable off!");
reachable = NO;
}
}];
return _sharedClient;
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (!self) {
return nil;
}
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
// Accept HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
[self setDefaultHeader:#"Accept" value:#"application/json"];
return self;
}
#end
I've debugged responses from the server and tested with hard coding two NSURLRequests simultaneously to the server. One for User A and one for User B, then printed the response data for both users.
On first login, User A login returned User A credentials. User B returned User B credentials. On second login, User A returned User A credentials, User B returned User A credentials. I have no idea how to fully disable cacheing.
Try:
[operation setCacheResponseBlock:^NSCachedURLResponse*(NSURLConnection* connection, NSCachedURLResponse* cachedResponse) {
return nil;
}];
And:
[request setCachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData];
The issue for me as suggested by SixteenOtto was a session being sent from the server and AFNetworking automatically using the cookie. I hadn't considered this before since we're using a restless API based on auth tokens, so a cookie for the session makes no sense. However, inspecting the HTTP response headers with Charles allowed me to see this.
Adding
[request setHTTPShouldHandleCookies:NO];
To my operation generator solved the issue.
The UIWebView does not automatically support processing of Passbook .pkpass files.
In this technical note, Apple recommend implementing a check via the UIWebViewDelegate methods to sniff out the MIME type and process it accordingly.
To add passes using a UIWebView, implement the appropriate
UIWebViewDelegate methods to identify when the view loads data with a
MIME type of application/vnd.apple.pkpass
However, I cannot find anything within the UIWebView Delegate Protocol Reference that is capable of providing the MIME type.
I can successfully download and process files directly using an NSURLConnection delegate with no problem, but what I wish to achieve is for passes to be properly processed if a user clicks on an Add To Passbook button while browsing within a UIWebView. Since I do not know the link, and many providers do not suffix their links with a .pkpass extension, following Apple's advice of examining the MIME type seems the best way to go.
I have tried adding the following
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)newRequest
navigationType:(UIWebViewNavigationType)navigationType
{
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[newRequest URL]];
// Spoof iOS Safari headers for sites that sniff the User Agent
[req addValue:#"Mozilla/5.0 (iPhone; CPU iPhone OS 6_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25" forHTTPHeaderField:#"User-Agent"];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:newRequest delegate:self];
return YES;
}
My NSURLConnection delegate:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSString *mime = [response MIMEType];
if ([mime isEqualToString:#"application/vnd.apple.pkpass"] && ![_data length]) {
_data = nil; // clear any old data
_data = [[NSMutableData alloc] init];
[_webPanel stopLoading];
}
}
-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
{
[_data appendData:data];
NSLog(#"Size: %d", [_data length]);
}
-(void)connectionDidFinishLoading:(NSURLConnection*)connection
{
if ([_data length]) {
PKAddPassesViewController *pkvc = [PassKitAPI presentPKPassFileFromData:_data];
pkvc.delegate = self;
[self presentViewController:pkvc
animated:YES
completion:nil];
}
}
The NSURLConnection delegates work fine when a connection is invoked directly, without the UIWebView. However, when I try launching an NSURLConnection from the UIWebView delegate the pass download fails because the only 80% or so of the .pkpass is being downloaded (I get a random mismatch of bytes in the _data variable and the Content-Length header).
So, my questions:
Is there an easier way to get hold of a MIME type, directly from the UIWebView Delegate methods?
If not, then am I going about this the right way with opening up a parallel NSURLConnection, or is there a better way?
If an NSURLConnection is the way to go, then what could be causing it to stop short of downloading the full file?
Just use js
let contentType = webView.stringByEvaluatingJavaScript(from: "document.contentType;")
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = request.URL;
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
[conn start];
return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSString *mime = [response MIMEType];
NSLog(#"%#",mime);
}
You could try subclassing NSURLProtocol and handling the response information parsing there.
Look at
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy
Don't forget to about subresources also using these hooks.
So this is my first post, I've found this site incredibly informative in my brief history with Objective C and iOS programming. Anyhow, I've run into a problem of sorts. A quick summary: I'm attempting to write a login form, which uses calls a custom class that with hit a webserver to auth using NSURLConnection. I'm using protocols and delegates to delegate back to the calling class to perform a segue to the main menu view controller once the authentication is complete.
The problem is that the menu I'm attempting to segue into takes anywhere from 6 to 75 seconds to display. If I remove the API call, it loads immediately. However, I'm doing logging throughout the process, and everything appears to step through at a normal pace. I even log when the menu view controller is loaded, and all logging happens normally. But the actual display of the menu is delayed!
Here are some code details:
View Controller Methods:
- (void) userLogin:(NSString *)userName password:(NSString *)password {
NSLog(#"VC login method");
api = [theAPI getSelf];
[api setDelegate:self];
[api userLogin:userName password:password];
}
- (void) userLoginDone:(BOOL)successful {
[self performSegueWithIdentifier:#"sgLoginToMainMenu" sender:self];
NSLog(#"Login Done");
}
API Method:
- (void) userLogin:(NSString *)userName password:(NSString *)password {
NSURL *url = [NSURL URLWithString:(NSString *) [API_PATH stringByAppendingString:#"test.html"]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSDictionary *json = [[JSON new] parseJSON:data];
self.usrID = [json objectForKey:#"usrID"];
self.sessionID = [json objectForKey:#"sessionID"];
self.userName = [json objectForKey:#"Username"];
NSLog(#"Username: %#", [json objectForKey:#"Username"]);
[[self delegate] userLoginDone:YES];
}];
}
All the NSLogs execute in a normal timespan (few milliseconds). Yet the main menu view controller takes entirely too long to appear! I'm very new to iOS programming, so I'm hoping I'm just overlooking something that googling couldn't solve. Any help would be greatly appreciated!
You need to update the UI on the main thread, but userLoginDone: is being called on an NSOperationQueue, which create its own separate thread. This could explain the delay in displaying. Have you tried using [NSOperationQueue mainQueue] (which returns the queue associated with the main thread) to pass to sendAsynchronousRequest: instead?
I am attempting to write a bit of code that checks the URL of a datasource, then populates an array with objects from that URL. It actually works well, but if there is a problem with the web connection or the address I want to populate the array with data from a bundled file. The issue I am having is that the connection didFailWithError method is never called. I tried passing a simple string but it does not call. I want the app to still function for people who are using ipod touch or are in airplane mode.
connection didReceiveResponse is working without issue.
This is what I'm working with.
- (void)loadListData{
NSLog(#"Loading data from sources");
NSURLRequest *listURLRequest = [NSURLRequest requestWithURL:integerPhoneListURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:1.0];
[[NSURLConnection alloc] initWithRequest:listURLRequest delegate:self];
if (!listConnectFail){
phoneListJSON =[NSData dataWithContentsOfURL:integerPhoneListURL];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:phoneListJSON waitUntilDone:YES];
} else {
//This will tell us if there is an error loading the file
NSLog(#"File not found on web init from file");
phoneListJSON =[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"contactlist" ofType:#"json"]];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:phoneListJSON waitUntilDone:YES];
}
//Initialize the filtered list with array of customer objects. Based on original data
filteredList = [[NSMutableArray alloc] init];
for (NSDictionary *dict in phoneListOriginal) {
contact *single = [[contact alloc] init];
single.fName = [dict objectForKey:#"fName"];
single.lName = [dict objectForKey:#"lName"];
single.extension = [dict objectForKey:#"extension"];
single.title = [dict objectForKey:#"title"];
single.department = [dict objectForKey:#"department"];
single.cellNumber = [dict objectForKey:#"cellNumber"];
//NSLog(#"%#", single.lName);
[filteredList addObject:single];
}
NSLog(#"Array filteredLIst contains %d records",[filteredList count]); }
-(void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
listConnectFail = YES;
NSLog(#"Connection Failed, pulling from file"); }
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
listConnectFail = NO;
NSLog(#"Connection Succeeded, populating from API");
}
I know it is probably something stupid that I am not seeing, but I could use the help to see what I don't
Thanks in advance!
How did you confirm that your delegate did not receive the message? Did you check the log?
Your code seems to assume that 'listConnectFail' will be set immediately after the NSURLConnection's init is done, which is not necessarily the case.
[[NSURLConnection alloc] initWithRequest:listURLRequest delegate:self];
if (!listConnectFail){...}
The NSURLConnection documentation states that 'The delegate will receive delegate messages as the load progresses.'
However, I am not sure about the airplane mode, maybe this particular error can be detected synchronously.
I'm trying to add a GHUnit test case to this SimpleHTTPServer example. The example include a Cocoa application that works fine for me. But I can't duplicate the behavior in a test case.
Here is the test class:
#import <GHUnit/GHUnit.h>
#import "SimpleHTTPServer.h"
#interface ServerTest : GHTestCase
{
SimpleHTTPServer *server;
}
#end
#implementation ServerTest
-(void)setUpClass
{
[[NSRunLoop currentRunLoop] run];
}
- (NSString*)requestToURL:(NSString*)urlString error:(NSError**)error
{
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:1];
NSURLResponse *response = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:error];
NSString *page = nil;
if (error == nil)
{
NSStringEncoding responseEncoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)[response textEncodingName]));
page = [[NSString alloc] initWithData:data encoding:responseEncoding];
[page autorelease];
}
return page;
}
- (void)testPortReuse
{
unsigned int port = 50001;
NSError *error = nil;
NSString *path, *url;
server = [[SimpleHTTPServer alloc] initWithTCPPort:port delegate:self];
sleep(10);
path = #"/x/y/z";
url = [NSString stringWithFormat:#"http://localhost:%u%#", port, path];
[self requestToURL:url error:&error];
GHAssertNil(error, #"%# : %#", url, error);
[server release];
}
- (void)processURL:(NSURL *)path connection:(SimpleHTTPConnection *)connection
{
NSLog(#"processURL");
}
- (void)stopProcessing
{
NSLog(#"stopProcessing");
}
#end
I've tried sending requests via NSURLRequest and also (during the sleep) via a web browser. The delegate methods -processURL and -stopProcessing are never called. The problem seems to be that [fileHandle acceptConnectionInBackgroundAndNotify] in SimpleHTTPServer -initWithTCPPort:delegate: is not causing any NSFileHandleConnectionAcceptedNotifications to reach the NSNotificationCenter -- so I suspect a problem involving run loops.
The problem seems to be with the NSFileHandle, not the NSNotificationCenter, because when [nc postNotificationName:NSFileHandleConnectionAcceptedNotification object:nil] is added to the end of initWithTCPPort:delegate:, the NSNotificationCenter does get the notification.
if (error == nil)
That should be:
if (data != nil)
error here is the passed-in pointer to an NSError* - it will only be nil if the caller passed nil instead of a reference to an NSError* object, which isn't what your -testPortReuse method does.
It would also be incorrect to dereference it (as in if (*error == nil)), because error arguments are not guaranteed to be set to nil upon error. The return value indicates an error condition, and the value returned in the error argument is only meaningful or reliable if there is an error. Always check the return value to determine if an error happened, then check the error parameter for details only if something did in fact go wrong.
In other words, as it's written above, your -requestToURL:error: method is incapable of handling success. Much like Charlie Sheen. :-)