I am taking over an old iOS project from developers no longer part of the project - the app is getting a rewrite and with that I am going to support iOS7 and upwards only.
So, I wanted to use AFNetworking 2.0 instead of ASIHTTPRequest - the reason behind this is NSURLSeesion. AFNetworking 2.0 supports NSURLSession and with that I can get my app to download content in the background at opportunistic times (According to Apple - NSURLSession must be used and Background Fetch mode turned on, for this to work? )
Let me start out by saying I am a new developer to iOS and networking stuff goes a little over my head - but I am determined to learn more about it and as much as I can. I have read AFNetworking documentation as well, but I fear since some of the terminology escapes me (Request, Response, Sterilisation, etc) - I am not grasping them 100%.
So, I took a look at the ASIHTTPRequest code the previous developer used to, from what I can see, build a GET / POST request - This is the code they used:
+ (ASIHTTPRequest*) buildRequest: (NSString*) url RequestType: (NSString*) requestType
PostData: (NSString*) postData
Host: (NSString*) host
ContentType: (NSString*) contentType
SoapAction: (NSString*) soapAction
RequestProperties: (NSDictionary*) requestProperties
{
NSURL *url = [NSURL URLWithString: url];
ASIHTTPRequest *request = [[[ASIHTTPRequest alloc] initWithURL:u] autorelease];
[request setDidFinishSelector:#selector(requestDone:)];
[request setDidFailSelector:#selector(requestWentWrong:)];
[request setTimeOutSeconds:20];
[request setQueuePriority:NSOperationQueuePriorityVeryHigh];
if (host != nil)
[request addRequestHeader: #"Host" value: host];
if (contentType != nil)
[request addRequestHeader: #"Content-Type" value: contentType];
if (soapAction != nil)
[request addRequestHeader: #"SOAPAction" value:soapAction];
if (requestType != nil)
[request setRequestMethod: requestType];
if (postData != nil)
{
NSMutableData* mPostData = [NSMutableData dataWithData:[postData dataUsingEncoding:NSUTF8StringEncoding]];
NSString *msgLength = [NSString stringWithFormat:#"%d", [postData length]];
[request setPostBody: mPostData];
[request addRequestHeader: #"Content-Length" value:msgLength];
}
if (requestProperties != nil)
{
for (int i = 0; i < [[requestProperties allKeys] count]; i++)
{
[request addRequestHeader:[[requestProperties allKeys] objectAtIndex: i] value: [requestProperties objectForKey:[[requestProperties allKeys] objectAtIndex: i]]];
}
}
return request;
}
I'm trying to understand this code and upgrade it to use AFNetworking V2.0 instead. I assume, just replacing ASIHTTPRequest with AFHTTPRequestOperation will not do the trick, correct?
I have been given some help and also managed to do a lot of digging around to see how I can get this right.
I made the method simpler - as I did not need Soap / Content-type, etc - just urlParamerters and some basic stuff:
This is the answer I came up with:
+ (AFHTTPSessionManager *) buildRequest: (NSString*) url RequestType: (NSString*) requestType PostDataValuesAndKeys: (NSDictionary*) postData RequestProperties: (NSDictionary*) requestProperties
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
if ([requestType isEqualToString:#"GET"])
{
[manager GET:url parameters:postData success:^(NSURLSessionDataTask *dataTask, id responseObject){
//Success
NSLog (#"Success");
NSData *xmlData = responseObject;
NSLog(#"Got XML Data: %#", xmlData);
}
failure:^(NSURLSessionDataTask *dataTask, NSError *error){
//Failure
NSLog (#"Failure");
}];
}else if ([requestType isEqualToString:#"GT"]){
[manager POST:url parameters:postData success:^(NSURLSessionDataTask *dataTask, id responseObject){
//Success
}
failure:^(NSURLSessionDataTask *dataTask, NSError *error){
//Failure
NSLog (#"Failure");
}];
}
return manager;
}
It will work for what I need it to do - but I am not sure if it's the best way to do it.
I couldn't see how I could detect the requestType other thank with looking at the NSString value. I looked into the AFHTTPSessionManager.h file for some clues on what to do with that - Matt suggests overriding the GET / POST methods if I want them done differently - per his comments in the header file:
Methods to Override
To change the behavior of all data task operation construction, which
is also used in the GET / POST / et al. convenience methods,
override dataTaskWithRequest:completionHandler:.
Also there is a requestSerializer property in that file - which you could use to detect the type of request - however it's implementation goes to the super class: AFURLSessionManager
In that class - there is a requestWithMethodmethod.
So, I tried to do this instead:
If I try implement that method - then I am not using the convince methods in AFHTTPSessionManager:
(NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(NSDictionary *)parameters
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;
Unless I have that completely wrong. After that I decided to just check the requestType using [NSString isEqualToString]
Related
I'm using AFNetworking for iOS and I want to send a request with a query parameter which has a datetime as a value. The wanted behavior should be:
Original: 2016-07-04T14:30:21+0200
Encoded: 2016-07-04T14%3A30%3A21%2B0200
Example: .../?datetime=2016-07-04T14%3A30%3A21%2B0200
AFNetworking does string encoding by itself which doesn't include special characters like + / & : and a few more (Wikipedia: Percent-encoding), which is fine since they are reserved.
So I have to encode the value of my datetime another way to escape the plus and colon sign. But when I manually encode the value before AFNetworking does it escapes the % twice obviously. So it puts a %25 for each %
2016-07-04T14%253A30%253A21%252B0200
I want AFNetworking to use percent encoding for the query with allowed characters like:
query.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLPathAllowedCharacterSet())
I didn't find a solution to change or disable the encoding by AFNetworking to do it completely manually. Do you have any suggestions?
After a little more research I've found a place to inject the encoding I want. This is the way it didn't work:
ENCODING NOT WORKING
Init the requestOperationManager:
self.requestOperationManager = [[AFHTTPRequestOperationManager alloc] init];
self.requestOperationManager.requestSerializer = [AFJSONRequestSerializer serializer];
Use the requestOperationManager to init operations
NSURLRequest *request = [NSURLRequest alloc] initWithURL:url]; // The problem is here
AFHTTPRequestOperation *operation = [self.requestOperationManager HTTPRequestOperationWithRequest:urlRequest success:^(AFHTTPRequestOperation *operation, id responseObject) {
// Success
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Failure
}];
[self.requestOperationManager.operationQueue addOperation:operation];
[operation start];
WAY TO HAVE MORE CONTROL
The AFHTTPRequestSerializer can also create requests and you can use your own serialization.
Init the requestOperationManager and add a query string serialization block:
self.requestOperationManager = [[AFHTTPRequestOperationManager alloc] init];
self.requestOperationManager.requestSerializer = [AFJSONRequestSerializer serializer];
[self.requestOperationManager.requestSerializer setQueryStringSerializationWithBlock:^NSString * _Nonnull(NSURLRequest * _Nonnull request, id _Nonnull parameters, NSError * _Nullable __autoreleasing * _Nullable error) {
if ([parameters isKindOfClass:[NSString class]]) {
NSString *yourEncodedParameterString = // What every you want to do with it.
return yourEncodedParameterString;
}
return parameters;
}];
Now change how you create your NSURLRequest:
NSString *method = #"GET";
NSString *urlStringWithoutQuery = #"http://example.com/";
NSString *query = #"datetime=2016-07-06T12:15:42+0200"
NSMutableURLRequest *urlRequest = [self.requestOperationManager.requestSerializer requestWithMethod:method URLString:urlStringWithoutQuery parameters:query error:nil];
It is important that you split your url. Use the url without the query for the URLString parameter and only the query for the parameters parameter. By using requestWithMethod:URLString:parameters:error it will call the query string serialization block you've provided above and encode the parameters as you want.
AFHTTPRequestOperation *operation = [self.requestOperationManager HTTPRequestOperationWithRequest:urlRequest success:^(AFHTTPRequestOperation *operation, id responseObject) {
// Success
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Failure
}];
[self.requestOperationManager.operationQueue addOperation:operation];
[operation start];
I'm using AFNetworking in my project as network layer.
For images I had some problems related to the caching and I updated the framework to 3.0 version.
During the update process I have some problems with requests which fetch the media content (images and videos).
This is my class :
+ (void)requestWithMethod:(NSString*)methodTypeValue path:(NSString*)pathValue parameters:(NSDictionary *)params token:(NSString*)token debug:(BOOL)debug completionHandler:(void (^)(id result, NSError *error))block {
if(debug){
NSLog(#"Method Type = %# \n",methodTypeValue);
NSLog(#"Link = %# \n",pathValue);
NSLog(#"Token = %#", token);
}
NSString *linkRequest = [NSString stringWithFormat:#"%#%#",GatewayRoot,pathValue];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSMutableURLRequest *req = [[AFJSONRequestSerializer serializer] requestWithMethod:methodTypeValue URLString:linkRequest parameters:nil error:nil];
[req setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
if (token) {
[req setValue:token forHTTPHeaderField:#"AccessToken"];
}
[[manager dataTaskWithRequest:req completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
if (!error) {
if (debug) {
NSLog(#"response:\n %# \n",responseObject);
}
block(responseObject,nil);
} else {
NSLog(#"Error: %#, %#, %#", error, response, responseObject);
block(nil, error);
}
}] resume];
}
I made test and everything is fine related to the normal requests.
I have to fetch some images and videos from server and I receive this error :
NSLocalizedDescription=Request failed: unacceptable content-type: image/png
NSLocalizedDescription=Request failed: unacceptable content-type: video/mp4
I tried to set the content types in this ways :
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:#"image/jpeg",#"video/mpeg",...nil];
manager.responseSerializer.acceptableContentTypes =[manager.responseSerializer.acceptableContentTypes setByAddingObject:#"image/jpeg",#"video/mpeg",...nil];
Please, can you help my how to set the acceptable content types ?
I have other problem related to image caching: with old version I loged in and I fetch the user profile image and after I loged out and relogin with other credentials I received the same image for user profile (I found that it was an issue realted to AFNetworking caching images).
Can you help me with the clear cache request flow ?
Thank a lot for your help !
Rather than setting the acceptable content types yourself, you probably just want to specify the appropriate responseSerializer. For example, if you want the UIImage, you can do:
manager.responseSerializer = [AFImageResponseSerializer serializer];
If you want the NSData, you can do:
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
If you don't want your request to be cached, you should set the req.cachePolicy to NSURLRequestReloadIgnoringCacheData or implement AFURLSessionManager cache response block, setDataTaskWillCacheResponseBlock. Or, of course, you could change your server so that those resources that shouldn't be cached have their header fields set accordingly.
Brand new to iOS development, so I'm feeling a little lost here. How can we use NSURLSession to upload an image to a web server (a PHP rest API that I'll setup)?
I've already been able to write the code to the point where a user is able to select an image, but now I need to send this image to my PHP web page that will handle it. Any advice on how to do this, and the best way to have my PHP setup to handle a request like this?
Any advice/insight would be greatly appreciated!
Abetter choice to use when interacting with web services is AFNetworking
Here is the code to upload an image
-(void)uploadImage:(UIImage *)image{
AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:[NSURL URLWithString:#"http://server.url"]];
NSData *imageData = UIImageJPEGRepresentation(image, 0.5);
NSDictionary *parameters = #{}; // pass parameter here
AFHTTPRequestOperation *op = [manager POST:#"{SERVER_URL}" parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
//do not put image inside parameters dictionary as I did, but append it!
[formData appendPartWithFileData:imageData name:paramNameForImage fileName:#"photo.jpg" mimeType:#"image/jpeg"];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success: %# \n%#", operation.responseString, responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %# \n%#", operation.responseString, error);
}];
[op start];
}
An NSURLSession works in conjunction with NSURLSessionTasks, which do the actual HTTP calls. These Tasks work asynchronously and deliver data in chunks, so you need some object to setup these tasks, put them to work and wait for their results. The class of this object needs to act as the delegate of the NSURLSessionTasks and therefore needs to implement some delegate protocols:
#interface MyUrlSessionTasksManager : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
...
#end
I have a class that does that. It keeps a list of running tasks and accumulates their retrieved chunked data until they're done. Then it returns its result to the requester, which is technically its delegate. At initialization it creates and configures an NSURLSession (I am using cookies) and an empty list of tasks:
- (id) init {
self = [super init];
if (self) {
self.runningTasks = [NSMutableDictionary dictionary];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.HTTPShouldSetCookies = YES;
configuration.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyAlways;
self.session = [NSURLSession sessionWithConfiguration: configuration
delegate: self
delegateQueue: [NSOperationQueue mainQueue]];
}
return self;
}
As mentioned, this class works with a delegate to which to report finished results. Here is the definition of the delegate:
#protocol MyUrlSessionTasksManagerDelegate <NSObject>
- (void) sessionTask: (NSURLSessionTask *) task completedWithError: (NSError *) error data: (NSData *) data;
#end
The main method of this class (for this case) is:
- (NSURLSessionTask *) startPostTaskForURL: (NSURL *) url parameters: (NSDictionary *) values {
NSURLRequest *request = [self createPostRequestWithURL:url parameters:values];
NSURLSessionTask *task = [self.session dataTaskWithRequest:request];
[self scheduleTask:task];
return task;
}
This methods takes an NSURL and an NSDictionary of parameters, creates an HTTP POST request, creates a Task with that request and schedules the task for execution. The scheduling is just putting the task in the dictionary 'runningTasks' with an associated Data object for accumulating the data received:
- (void) scheduleTask: (NSURLSessionTask *) task {
[self.runningTasks setObject:[NSMutableData data] forKey:task];
[task resume];
}
Whenever there is data for a task, the following TaskDelegate methods is called. All it does is lookup the task in its list of running tasks and append the data received to the tasks associated Data object:
- (void) URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSMutableData *runningData = [self dataForTask:dataTask];
if (!runningData) {
NSLog(#"No data found for task");
}
[runningData appendData: data];
}
When the data reception is completed the following TaskDelegate method will be called. All it does is report to its delegate, passing the whole Data object and any errors:
- (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
[self.delegate sessionTask:task completedWithError:error data:[self dataForTask:task]];
}
Some internal support methods used:
- (NSMutableData *) dataForTask: (NSURLSessionTask *) task {
return [self.runningTasks objectForKey:task];
}
- (NSString *)urlEncodedUTF8String: (NSString *) source {
return (id)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(0, (CFStringRef)source, 0,
(CFStringRef)#";/?:#&=$+{}<>,", kCFStringEncodingUTF8));
}
- (NSString *) createHttpParameters: (NSDictionary *) parameters {
NSMutableString *body = [NSMutableString string];
for (NSString *key in parameters) {
NSString *val = [parameters objectForKey:key];
if ([body length])
[body appendString:#"&"];
[body appendFormat:#"%#=%#", [self urlEncodedUTF8String: [key description]],
[self urlEncodedUTF8String: [val description]]];
}
return body;
}
- (NSURLRequest *)createPostRequestWithURL:(NSURL *)url
parameters:(NSDictionary *)parameters {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
[request addValue:#"application/x-www-form-urlencoded"
forHTTPHeaderField:#"Content-Type"];
NSString * httpParams = [self createHttpParameters:parameters];
[request setHTTPBody:[httpParams dataUsingEncoding:NSUTF8StringEncoding]];
return request;
}
What is not provided for in this code is how to get the file and convert it to the proper encoding to pass it as a parameter in the POST body. Reason is this code was copied literally from one of my projects, which is not using file uploads. Still I hope it helps you.
I have an iOS + Rails 3.1 app, and I'm using AFIncrementalStore for the client-server communication.
I have implemented Token Authentication on my Rails server according to this tutorial: http://matteomelani.wordpress.com/2011/10/17/authentication-for-mobile-devices/
I now want to include the &auth_token=XXXXXXXX in every request from client to server, including POST requests. How would I do that? I haven't found the solution in this related post: Using AFIncrementalStore with an Auth token
UPDATE: this is my first code attempt, but doesn't seem to send the auth_token:
(inside my AFIncrementalStoreHTTPClient sub-class)
- (NSMutableURLRequest *)requestForFetchRequest:(NSFetchRequest *)fetchRequest withContext:(NSManagedObjectContext *)context {
NSMutableURLRequest *request = [[super requestForFetchRequest:fetchRequest withContext:context] mutableCopy];
NSMutableString *requestBody = [[NSMutableString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding];
[requestBody appendFormat:#"&%#=%#", #"auth_token", #"xkT2eqqdoNp5y4vQy7xA"];
[request setHTTPBody:[requestBody dataUsingEncoding:NSUTF8StringEncoding]];
return request;
}
UPDATE: I skimmed your question (sorry!), and my sample code below works for a regular AFHTTPClient, but not AFIncrementalStore. The same basic approach will work, though, and there's sample code at this answer that should point you in the right direction.
You can't just append &auth_token=whatever to the end of your HTTP body in all cases.
You probably want to override your getPath... and postPath... methods with something like:
- (void)getPath:(NSString *)path
parameters:(NSDictionary *)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
if (parameters) {
// Make a mutable copy and add the "token" parameter to the dictionary
NSMutableDictionary *mutableParams = [parameters mutableCopy];
[mutableParams setObject:#"whatever" forKey:#"token"];
parameters = [NSDictionary dictionaryWithDictionary:mutableParams];
} else {
parameters = #{#"token" : #"whatever"};
}
[super getPath:path parameters:parameters success:success failure:failure];
}
This approach will allow AFNetworking to appropriately encode your parameters depending on your specific request and encoding settings.
If you are rolling your own AFHTTPRequestOperation objects instead of using the convenience methods (you probably aren't), just make sure you include the token in parameters before you create your NSURLRequest like so:
NSURLRequest *request = [self requestWithMethod:#"GET" path:path parameters:parameters];
First of all, I realize that for a PUT request the request parameters should be passed in the request body. However, I am working with an API (which I am only consuming, not developing) that expects the request parameters to be appended as the query string for a PUT request.
I am making use of a subclass of AFHTTPClient. For the particular PUT request that I am referring to, I make use of getPath:parameters:success:failure:.
The solution that I have found so far is manually constructing the path variable to include the parameters I want to pass. Of course, this is not ideal and I was wondering if there is another option that is less error prone.
In short, is there a way to send a PUT request using AFHTTPClient (a subclass of) with the passed parameters appended (and encoded) as the query string (just like a GET request)?
The getPath:parameters:success:failure method inside AFHTTPClient.m calls requestWithMethod:path:parameters. Inside the latter method, the HTTP method is checked against certain values to decide how to append the parameters to the request. As you can see, by default, the parameters should only be appended to the URL in case of a GET, HEAD or DELETE request. Since you need them to be appended to the URL in case of a PUT request too, modify the requestWithMethod:path:parameters like this:
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
path:(NSString *)path
parameters:(NSDictionary *)parameters
{
NSURL *url = [NSURL URLWithString:path relativeToURL:self.baseURL];
NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] initWithURL:url] autorelease];
[request setHTTPMethod:method];
[request setAllHTTPHeaderFields:self.defaultHeaders];
if ([method isEqualToString:#"GET"] || [method isEqualToString:#"HEAD"]) {
[request setHTTPShouldUsePipelining:YES];
}
if (parameters) {
if ([method isEqualToString:#"GET"] || [method isEqualToString:#"HEAD"] || [method isEqualToString:#"DELETE"] || [method isEqualToString:#"PUT"]) {
url = [NSURL URLWithString:[[url absoluteString] stringByAppendingFormat:[path rangeOfString:#"?"].location == NSNotFound ? #"?%#" : #"&%#", AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding)]];
[request setURL:url];
} else {
NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(self.stringEncoding));
switch (self.parameterEncoding) {
case AFFormURLParameterEncoding:;
[request setValue:[NSString stringWithFormat:#"application/x-www-form-urlencoded; charset=%#", charset] forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:[AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding) dataUsingEncoding:self.stringEncoding]];
break;
case AFJSONParameterEncoding:;
[request setValue:[NSString stringWithFormat:#"application/json; charset=%#", charset] forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:[AFJSONStringFromParameters(parameters) dataUsingEncoding:self.stringEncoding]];
break;
case AFPropertyListParameterEncoding:;
[request setValue:[NSString stringWithFormat:#"application/x-plist; charset=%#", charset] forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:[AFPropertyListStringFromParameters(parameters) dataUsingEncoding:self.stringEncoding]];
break;
}
}
}
return request;
}
You can just do what the code in datwalk's answer is doing without modifying the underlying AFNetworking code. Use AFNetworking to create a path that includes URL parameters:
NSDictionary *mutableParameters = [[NSMutableDictionary alloc] initWithObjectsAndKeys:#"csv",#"format", #"0",#"level", #"2013-10-25", #"keydate", nil];
NSString *urlPath = [NSString stringWithFormat:#"applications/%#/Planning?%#", name, AFQueryStringFromParametersWithEncoding(mutableParameters, NSUTF8StringEncoding)];
NSString *apiFunctionPath = [urlPath stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[self putPath:apiFunctionPath
parameters:nil
success:^(AFHTTPRequestOperation *operation, id XML) {
NSLog(#"%#",XML);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%#",error);
}];
For the case of my API, changing the encoding of the AFHTTPClient object solved the issue:
myAFHTTPClient.parameterEncoding = AFJSONParameterEncoding;