NSURL Caching issues - objective-c

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.

Related

What is the appropriate NSURLConnection/Credential pattern for doing NTLM web service calls?

I have a "traditional" enterprise iPad application that needs to make many different web service calls over its lifetime using NTLM authentication. Upon start up of the application, I anticipate getting the user name and password off of a keychain (which the app saves the first time its used since the keychain does not have the user name and subsequently is updated when the password fails to work due to updates).
On startup, various web service calls are needed to get initial data for the application. The user then will be presented with a tabbed controller to choose the functionality they want which in turn will, of course, do more web service calls.
I believe I have a tactic for dealing with each class receiving data through a custom data delegate as presented in this StackOverflow answer (How do you return from an asynchronous NSURLConnection to the calling class?). However, I'm still a bit confused as to how to properly use the -(void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge functionality.
In didReceiveAuthenticationChallenge, I have code like this
[[challenge sender] useCredential:[NSURLCredential credentialWithUser:#"myusername"
password:#"mypassword"
persistence:NSURLCredentialPersistencePermanent]
forAuthenticationChallenge:challenge];
Since I'm setting permanent persistence, I would expect to not to have to constantly pass in the user name and password in the functionality. Is there a pattern that's used to initially setup the user's NTLM credentials (and/or check to see if they're already there) and then just use the "permanent" credentials for subsequent web service calls?
Also, as a secondary question/part to this. What is the appropriate/elegant approach to passing around a username/password throughout an Objective-C application? I'm thinking either global var's or a singleton instance (which seems a bit overkill for just a couple of needed var's).
It has been awhile since we've tackled this issue and successfully solved it. I thought it was time to put up the answer here. The code below belongs in its own class and will not work quite out of the box but should get you a long ways towards what you need. For the most part, it all should work fine, but you'll just need to make sure the various areas such as alert views, data stores, etc. are all set up the way you need.
A major stumbling block in our understanding of the way that Objective-C & iOS deals with NTLM communications is figuring out its normal process of communicating with a URL.
First contact with a URL is done anonymously. Of course, in a Windows secure environment this will fail. This is when the application will attempt to contact the URL again but this time with any credentials it has for that URL already on the keychain and utilize the willSendRequestForAuthenticationChallenge method. This was very confusing for us since this method didn't fire until AFTER the first call failed. It finally dawned on us what was happening with that first call being anonymous.
Part of the pattern you'll see here is that the connection will be attempted with any credentials already on the keychain. If those fail/missing, then we will popup a view that requests the user to enter the username and password and then we retry with that.
There's a number of idiosyncrasies that we needed to account for as you'll see throughout the code. It took many iterations and lots of testing to get this stable. Much of this was based on patterns that have been posted all over the internet for doing pretty much what we were trying to do but didn't quite take us all the way there.
The code we did generalizes GET/POST calls. This is my first major code post to StackOverflow and my apologies if I'm missing some conventions and I'll correct what I need to when brought to my attention.
#import "MYDataFeeder.h"
#import "MYAppDelegate.h"
#import "MYDataStore.h"
#import "MYAuthenticationAlertView.h"
#import "MYExtensions.h"
#interface MYDataFeeder () <NSURLConnectionDelegate>
#property (strong, nonatomic) void (^needAuthBlock)(NSString *, NSString *);
#property (strong, nonatomic) void (^successBlock)(NSData *);
#property (strong, nonatomic) void (^errorBlock)(NSError *);
#end
#implementation MYDataFeeder{
NSMutableData *_responseData;
NSString *_userName;
NSString *_password;
NSString *_urlPath;
BOOL _hasQueryString;
}
+ (void)get: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
{
MYDataFeeder *x = [[MYDataFeeder alloc] initWithGetRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
}
+ (void)post: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
jsonString: (NSString *)jsonString
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
{
MYDataFeeder *x = [[MYDataFeeder alloc] initWithPostRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials jsonString:jsonString successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
}
- (instancetype)initWithGetRequest: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
{
return [self initWithRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials isPost:NO jsonString:nil successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
}
-(instancetype)initWithPostRequest: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
jsonString: (NSString *)jsonString
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
{
return [self initWithRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials isPost:YES jsonString:jsonString successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
}
//Used for NTLM authentication when user/pwd needs updating
- (instancetype)initWithRequest: (NSString *)requestString
userName: (NSString *)userName
password: (NSString *)password
hasNewCredentials: (BOOL)hasNewCredentials
isPost: (BOOL)isPost
jsonString: (NSString *)jsonString
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock //delegate:(id<MYDataFeederDelegate>)delegate
{
self = [super init];
requestString = [requestString stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
if(self) {
if (!errorBlock || !successBlock || !needAuthBlock) {
[NSException raise:#"MYDataFeeder Error" format:#"Missing one or more execution blocks. Need Success, Error, and NeedAuth blocks."];
}
_responseData = [NSMutableData new];
_userName = userName;
_password = password;
_successBlock = successBlock;
_hasNewCredentials = hasNewCredentials;
_errorBlock = errorBlock;
_needAuthBlock = needAuthBlock;
NSString *host = [MYDataStore sharedStore].host; //Get the host string
int port = [MYDataStore sharedStore].port; //Get the port value
NSString *portString = #"";
if (port > 0) {
portString = [NSString stringWithFormat:#":%i", port];
}
requestString = [NSString stringWithFormat:#"%#%#/%#", host, portString, requestString];
NSURL *url = [NSURL URLWithString:requestString];
NSString *absoluteURLPath = [url absoluteString];
NSUInteger queryLength = [[url query] length];
_hasQueryString = queryLength > 0;
_urlPath = (queryLength ? [absoluteURLPath substringToIndex:[absoluteURLPath length] - (queryLength + 1)] : absoluteURLPath);
NSTimeInterval timeInterval = 60; //seconds (60 default)
NSMutableURLRequest *request;
if (isPost) {
request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:timeInterval];
NSData *requestData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
[request setHTTPMethod:#"POST"];
[request setValue:#"application/json" forHTTPHeaderField:#"Accept"];
[request setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
[request setValue:[NSString stringWithFormat:#"%lu", (unsigned long)requestData.length] forHTTPHeaderField:#"Content-Length"];
[request setHTTPBody: requestData];
[request setHTTPShouldHandleCookies:YES];
}
else {
request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:timeInterval];
}
NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
return self;
}
- (instancetype)initWithRequest: (NSString *)requestString
successBlock: (void (^)(NSData *))successBlock
errorBlock: (void (^)(NSError *))errorBlock
needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock //delegate:(id<MYDataFeederDelegate>)delegate
{
return [self initWithRequest:requestString userName:NULL password:NULL hasNewCredentials:NO isPost:NO jsonString:nil successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock]; //delegate:delegate];
}
#pragma mark - Connection Events
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
return YES;
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
if (response){
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSInteger code = httpResponse.statusCode;
if (code == 401){
NSLog(#"received 401 response");
[MYAuthenticationAlertView showWithCallback:_needAuthBlock];
[connection cancel];
}
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_successBlock(_responseData);
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[_responseData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
_errorBlock(error);
}
- (void) connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodNTLM])
{
BOOL hasConnectionCredentials = [[MYDataStore sharedStore] hasConnectionCredentials]; //Determines if there's already credentials existing (see method stub below)
long previousFailureCount = [challenge previousFailureCount];
BOOL hasFailedAuth = NO;
//If the application has already gotten credentials at least once, then see if there's a response failure...
if (hasConnectionCredentials){
//Determine if this URL (sans querystring) has already been called; if not, then assume the URL can be called, otherwise there's probably an error...
if ([[MYDataStore sharedStore] isURLUsed:_urlPath addURL:YES] && !_hasQueryString){
NSURLResponse *failureResponse = [challenge failureResponse];
if (failureResponse){
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)[challenge failureResponse];
long code = [httpResponse statusCode];
if (code == 401){
hasFailedAuth = YES;
}
}
}
}
else{
//Need to get user's credentials for authentication...
NSLog(#"Does not have proper Credentials; possible auto-retry with proper protection space.");
}
/* This is very, very important to check. Depending on how your security policies are setup, you could lock your user out of his or her account by trying to use the wrong credentials too many times in a row. */
if (!_hasNewCredentials && ((previousFailureCount > 0) || hasFailedAuth))
{
NSLog(#"prompt for new creds");
NSLog(#"Previous Failure Count: %li", previousFailureCount);
[[challenge sender] cancelAuthenticationChallenge:challenge];
[MYAuthenticationAlertView showWithCallback:_needAuthBlock];
[connection cancel];
}
else
{
if (_hasNewCredentials){
//If there's new credential information and failures, then request new credentials again...
if (previousFailureCount > 0) {
NSLog(#"new creds failed");
[MYAuthenticationAlertView showWithCallback:_needAuthBlock];
[connection cancel];
} else {
NSLog(#"use new creds");
//If there's new credential information and no failures, then pass them through...
[[challenge sender] useCredential:[NSURLCredential credentialWithUser:_userName password:_password persistence:NSURLCredentialPersistencePermanent] forAuthenticationChallenge:challenge];
}
} else {
NSLog(#"use stored creds");
//...otherwise, use any stored credentials to call URL...
[[challenge sender] performDefaultHandlingForAuthenticationChallenge:challenge];
}
}
}
else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { // server trust challenge
// make sure challenge came from environment host
if ([[MYDataStore sharedStore].host containsString:challenge.protectionSpace.host]) {
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
}
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
else {
// request has failed
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
#end
-(BOOL) hasConnectionCredentials
{
NSDictionary *credentialsDict = [[NSURLCredentialStorage sharedCredentialStorage] allCredentials];
return ([credentialsDict count] > 0);
}
//Sample use of Data Feeder and blocks:
-(void)myMethodToGetDataWithUserName:(NSString*)userName password:(NSString*)password{
//do stuff here
[MYDataFeeder get:#"myURL"
userName:userName
password:password
hasNewCredentials:(userName != nil)
successBlock:^(NSData *response){ [self processResponse:response]; }
errorBlock:^(NSError *error) { NSLog(#"URL Error: %#", error); }
needAuthBlock:^(NSString *userName, NSString *password) { [self myMethodToGetDataWithUserName:username withPassword:password]; }
];
}
//The needAuthBlock recalls the same method but now passing in user name and password that was queried from within an AlertView called from within the original DataFeeder call

Switching from AFNetworking to RestKit

I started developing my application using AFNetworking. Everything went OK till I want to use core data. I know there is an additional class (AFIncrementalStore) for that. But because I'm new to IOS-development and there is not a lot of information about that. I decided to switch to RestKit because here is a lot more information. Now, I followed a tutorial about AFNetworking. Here I created an API class which this method in it.
+(API *)sharedInstance
{
static API *sharedInstance = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^ {
sharedInstance = [[self alloc]initWithBaseURL:[NSURL URLWithString:kAPIHost]];
});
return sharedInstance;
}
#pragma mark - init
//intialize the API class with the destination host name
-(API *)init
{
//call super init
self = [super init];
if (self != nil){
//initialize the object
user = 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;
}
-(void)loginCommand:(NSMutableDictionary *)params onCompletion:(JSONResponseBlock)completionBlock{
NSLog(#"%#%#",kAPIHost,kAPILogin);
NSMutableURLRequest *apiRequest = [self multipartFormRequestWithMethod:#"POST" path:kAPILogin parameters:params constructingBodyWithBlock:^(id <AFMultipartFormData>formData){
//TODO: attach file if needed
}];
AFJSONRequestOperation *operation = [[AFJSONRequestOperation alloc] initWithRequest:apiRequest];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject){
//success!
NSLog(#"SUCCESSSS!");
completionBlock(responseObject);
}failure:^(AFHTTPRequestOperation *operation, NSError *error){
//Failure
NSLog(#"FAILUREE!");
completionBlock([NSDictionary dictionaryWithObject:[error localizedDescription] forKey:#"error"]);
}];
[operation start];
}
This handles the communication between my webservice and application.
In the viewControler itself I call this method like this.
/* [[API sharedInstance] loginCommand:[NSMutableDictionary dictionaryWithObjectsAndKeys:_txtLogin.text,#"email",_txtPass.text,#"pwd", nil] onCompletion:^(NSDictionary *json){
//completion
if(![json objectForKey:#"error"]){
NSLog(#"status %#",[json valueForKeyPath:#"data.status"]);
if([[json valueForKeyPath:#"data.status"]intValue] == 200){
// Everything is oké, and login is succesfull
}else{
//show validation
}
}else {
NSLog(#"Cannot connect to the server");
}
}];*/
This is how I do this in AFnetworking. But what are the differences when I do this in RestKit. I searched for tutorials. But after the update from RestKit 1.0 to 2.0 a lot of these tutorials are outdated. So I hope anybody can help me out with this!
Kind regards!
I used this tutorial for using RestKit. It shows you how to use it and you can learn the other details. http://www.youtube.com/watch?v=dFi9t8NW0oY

changing AFNetworking baseURL

I am using AFNetworking with the singleton model suggested in their example.
+ (SGStockRoomHTTPClient *)sharedClient
{
static SGStockRoomHTTPClient *_sharedClient = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
NSString *baseUrlString = [[NSUserDefaults standardUserDefaults] stringForKey:#"server_root_url_preference"];
_sharedClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:baseUrlString]];
});
return _sharedClient;
}
- (id)initWithBaseURL:(NSURL *)url {
self = [super initWithBaseURL:url];
if (!self) {
return nil;
}
[self registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self setDefaultHeader:#"Accept" value:#"text/html"];
return self;
}
Initialization is done with a baseURL taken from the user defaults.
My problem is that the baseURL property is read-only. If the user goes to settings and changes the baseURL user default, how can I change it in my client?
Another similar case I have with a need to change the baseURL is an API which requires multiple calls and logic to determine to the right baseURL. And the base url can still change while the app is running (e.g. user changes networking environment requiring a change from local connection to 3G connection via external proxy server.).
I see why the baseURL property is read-only: there are things like networkReachabilityStatus that run in the background and are tied to that setting. This said, it seems fairly easy to have a setBaseURL method that stops monitoring, changes the value, then starts monitoring again...
I guess my design is not right, should I give-up the singleton in this case and re-create the client each time the baseURL should change?
The class AFHTTPClient is designed to work with a single base URL. This is why it is readonly.
Here are some solutions if you have more than one base URL:
in your AFHTTPClient subclass override the property qualifier. Place this inside the #interface: #property (readwrite, nonatomic, retain) NSURL *baseURL;
Don't use AFHTTPClient at all
Create multiple instances of AFHTTPClient
When you create HTTP operations you can override the baseURL. Just set the full URL in the path instead of a relative path.
Hope this helps you.
#interface EFApiClient : AFHTTPSessionManager
#property (nonatomic,assign)BOOL isTestEnvironment ;
+ (instancetype)sharedMClient;
#end
#implementation EFApiClient
+ (instancetype)sharedMClient
{
if ([EFApiClient sharedClient].isTestEnvironment) {
return [EFApiClient sharedTestClient] ;
}
else{
return [EFApiClient sharedClient];
}
}
+ (instancetype)sharedClient
{
static EFApiClient *_sharedMClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedMClient = [[EFApiClient alloc] initWithBaseURL:[NSURL URLWithString:#"https://xxx.xxx.com"]];
[EFApiClient clientConfigWithManager:_sharedMClient];
});
return _sharedMClient;
}
+ (instancetype)sharedTestClient
{
static EFApiClient *_sharedMClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedMClient = [[EFApiClient alloc] initWithBaseURL:[NSURL URLWithString:#"https://test.xxx.xxx.com"]];
[EFApiClient clientConfigWithManager:_sharedMClient];
});
return _sharedMClient;
}
+ (void)clientConfigWithManager:(EFApiClient *)client
{
AFSecurityPolicy* policy = [[AFSecurityPolicy alloc] init];
[policy setAllowInvalidCertificates:YES];
[policy setValidatesDomainName:NO];
[client setSecurityPolicy:policy];
client.requestSerializer = [AFHTTPRequestSerializer serializer];
client.responseSerializer = [AFHTTPResponseSerializer serializer];
//client.requestSerializer.HTTPMethodsEncodingParametersInURI = [NSSet setWithArray:#[#"POST", #"GET", #"HEAD"]];
client.responseSerializer = [AFJSONResponseSerializer serializer];
client.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:#"application/x-javascript",#"application/json", #"text/json", #"text/html", nil];
}
#end

Using objective-c blocks recursively with iOS Twitter API

So I'm trying to use the built in Twitter API in iOS 5 to retrieve a list of all the followers for a given user. In all the example documentation I can find, requests are made to the API passing inline blocks to be executed when the request returns, which is fine for most of the simpler stuff, BUT when I'm trying to get ~1000 followers, and the request is returning them paged in sizes ~100, I'm stuck on how to recursively call the request again using the 'next paging address' returned and processed inside the completion block. Here is the code:
- (void)getTwitterFollowers {
// First, we need to obtain the account instance for the user's Twitter account
ACAccountStore *store = [[ACAccountStore alloc] init];
ACAccountType *twitterAccountType =
[store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
// Request access from the user for access to his Twitter accounts
[store requestAccessToAccountsWithType:twitterAccountType
withCompletionHandler:^(BOOL granted, NSError *error) {
if (!granted) {
// The user rejected your request
NSLog(#"User rejected access to his account.");
}
else {
// Grab the available accounts
NSArray *twitterAccounts =
[store accountsWithAccountType:twitterAccountType];
if ([twitterAccounts count] > 0) {
// Use the first account for simplicity
ACAccount *account = [twitterAccounts objectAtIndex:0];
// Now make an authenticated request to our endpoint
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
[params setObject:#"1" forKey:#"include_entities"];
// The endpoint that we wish to call
NSURL *url = [NSURL URLWithString:#"http://api.twitter.com/1/followers.json"];
// Build the request with our parameter
request = [[TWRequest alloc] initWithURL:url
parameters:params
requestMethod:TWRequestMethodGET];
[params release];
// Attach the account object to this request
[request setAccount:account];
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
if (!responseData) {
// inspect the contents of error
FullLog(#"%#", error);
}
else {
NSError *jsonError;
followers = [NSJSONSerialization JSONObjectWithData:responseData
options:NSJSONReadingMutableLeaves
error:&jsonError];
if (followers != nil) {
// THE DATA RETURNED HERE CONTAINS THE NEXT PAGE VALUE NEEDED TO REQUEST THE NEXT 100 FOLLOWERS,
//WHAT IS THE BEST WAY TO USE THIS??
FullLog(#"%#", followers);
}
else {
// inspect the contents of jsonError
FullLog(#"%#", jsonError);
}
}
}];
} // if ([twitterAccounts count] > 0)
} // if (granted)
}];
[store release];
}
Ideally I'd like some way to listen for this data being returned, check for a next page value and if it exists, reuse the code block and append the data returned. I', sure there must be a 'best-practice' way to achieve this, any help would be much appreciated!
To use any block recursively you have to declare it first and define it later. Try this:
__block void (^requestPageBlock)(NSInteger pageNumber) = NULL;
requestPageBlock = [^(NSInteger pageNumber) {
// do request with some calculations
if (nextPageExists) {
requestPageBlock(pageNumber + 1);
}
} copy];
// now call the block for the first page
requestPageBlock(0);
To expand on #Eimantas' answer, your request handler is expecting a specific block signature, so you need a different way to handle the page number.
-(void)getTwitterFollowers {
// set up request...
__block int page = 0;
__block void (^requestHandler)(NSData*, NSHTTPURLResponse*, NSError*) = null;
__block TWRequest* request = [[TWRequest alloc] initWithURL:url
parameters:params
requestMethod:TWRequestMethodGET];
requestHandler = [^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
followers = [NSJSONSerialization JSONObjectWithData:responseData
options:NSJSONReadingMutableLeaves
error:&jsonError];
if (followers != nil) {
// process followers
page++;
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithDictionary:request.parameters];
// update params with page number
request = [[TWRequest alloc] initWithURL:url
parameters:params
requestMethod:TWRequestMethodGET];
[request performRequestWithHandler:requestHandler];
}
} copy];
// now call the block for the first page
[request performRequestWithHandler:requestHandler];
}

UIWebView iOS5 changing user-agent

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;
}