I'm creating an app where I would like to use cached responses from time to time. I ran into a weird issue related to NSURLCache, more spicifically, if I set NSURLRequestReturnCacheDataDontLoad on my request, I don't get a cached response on iOS 8. Here's the code I'm using with AFNetworking to get this working:
// Define web request.
void (^simpleBlock)(void) = ^{
GTSessionManager.manager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
[GTSessionManager.manager POST:string parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *responseJson = responseObject;
[self parseJsonSuccess:responseJson];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if (error.userInfo[JSONResponseSerializerWithDataKey]) {
NSData *data = error.userInfo[JSONResponseSerializerWithDataKey];
NSDictionary* json = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:&error];
[self parseJsonError:json];
}
else
[self parseJsonError:nil];
}];
};
if (self.shouldUseCache) {
GTSessionManager.manager.requestSerializer.cachePolicy = NSURLRequestReturnCacheDataDontLoad;
[GTSessionManager.manager POST:string parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *responseJson = responseObject;
[self parseJsonCacheSuccess:responseJson];
// Load second request.
simpleBlock();
} failure:^(NSURLSessionDataTask *task, NSError *error) {
simpleBlock();
}];
}
else
simpleBlock();
The main idea here is that if the the client wants to use a cache, the first request should try to load it, pass the result to the handler and start reloading the request to refresh the cache.
This approach works well on iOS 7 but doesn't work on iOS 8+. I've setup the NSURLCache with the following call.
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:500 * 1024 * 1024
diskCapacity:500 * 1024 * 1024
diskPath:#"cache" /**Tried nil here as well. */];
[NSURLCache setSharedURLCache:sharedCache];
I read a few articles and other SO questions about this but I just can't get this to work. Am I missing something? Could it be related to the fact that I'm using a POST request?
EDIT
Forgot to mention that I'm not using a cache policy on my server. I also tried setting a cache policy using Cache-Control=max-age=604800, public but I got the same behavior.
Alright, so after some further investigation, I concluded that the problem is somewhere in NSURLSession. I sent the exact same requests as above but using AFHTTPRequestOperation instead of AFHTTPSessionManager and the cache loads perfectly on iOS 8+.
Not quite sure where the problem is still though, I think it has something to do with the cache policies but for now I'm sticking with NSURLConnection.
Related
So I'm trying to do a post request with an array of JSON parameters sent to a server, here's the code for that
for(USER_ACTIONS *ua in [USER_ACTIONS listRegisterdActions]){
//Create a single JSON object here
[array addObject:jsonString];
}
NSString *dataString = [NSString stringWithFormat:#"[%#]",array.count ?
NSData* data = [dataString dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[#"data"] = data;
[self POST:#"?cmd=log" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];
This works with a single JSON object, but once there are more of them I get the following exception
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid type in JSON write (NSConcreteMutableData)'
It's out of the question to do this with a multiple post requests so I need a way to do this with one, my search results on this have not gotten any clear results on how to do this with AFNetworking 2.x so I'd appreciate some pointers on where to go with this.
AFNetworking can automatically change paramter in NSDictionary to JSON.
change your manager's property requestSerializer to AFJSONRequestSerializer the default value is AFHTTPRequestSerializer
AFJSONRequestSerializer is a subclass of AFHTTPRequestSerializer that encodes parameters as JSON using NSJSONSerialization, setting the Content-Type of the encoded request to application/json.
For some reason just using the AFHTTPSessionManager Post function with the constructingBodyWithBlock parameter actually works for some reason. No idea why this is needed because there isn't actually anything in that block, if anyone could tell me why it would be nice.
[self POST:#"?cmd=log" parameters:parameters constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
//For some reason the request won't work unless this block is included in the function as well, even though nothing is actually done in it
} success:^(NSURLSessionDataTask *task, id responseObject) {
//Success code here
} failure:^(NSURLSessionDataTask *task, NSError *error) {
}];
I was wondering, is there a file format that allows you to store keys and values, and some api that lets you read said file from a web server into a NSDictionary easily? I'm just trying to store 5 or so values associated with keys in a file on the web, so looking for something simple.
I know how to read a simple text file into an NSString, but I was wondering if there is something that lets me do what I described above easily / more efficiently without having to manually prepare the file myself and then write the code to tokenize it, etc.
The file format you need is json. The API you need is AFNetworking. Here is the sample which connect to a server and parse that json to NSDictionary.
//send son info to server
NSDictionary* json = #{
kUserTokenKey : [SOLUser currentUser].token,
kMarkerUserMarkPayIDKey : markerUserMark.objectID,
kCardMarkerPayIDKey : card.objectID,
kCardZipCodeKey: zipCode
};
//create a request url
NSString* requestURL = [NSString stringWithFormat:#"%#%#",kSOLServicesURL,kSOLServicesMarkerPay];
//AFNetworking block to call server api
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST:requestURL parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
[manager invalidateSessionCancelingTasks:YES];
//Recieving object from server and pass to json
NSDictionary* obj = responseObject;
if ([obj[kAPIStatusKey] isEqualToString:kAPIStatusSuccessKey]) {
if (completion) completion(nil);
} else {
NSError* error = [NSError errorWithMessage:obj[kAPIDataKey] code:0];
if (completion) completion(error);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[manager invalidateSessionCancelingTasks:YES];
LBLog(#"Error: %#", error);
if (completion) completion(error);
}];
I have an app where I'm downloading quite a few images for display later on.
The app needs to function even when there's no internet connection, so the images are loaded at one point and persisted using NSURLCache.
This is a neat solution since I can use normal networking libraries and easily take advantage of custom cache settings.
I realize that this type of caching doesn't guarantee that the files are persisted until they expire since it's up to the system to release cache whenever it deems necessary. That's not a huge issue since the images should be able to be re-downloaded and hence re-persisted.
However, I've noticed that it decides to randomly release images from cache, and it doesn't seem to persist them when I download them again. I'm not sure where I'm going wrong.
First of all, I define the cache capacity like so:
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:50 * 1024 * 1024
diskCapacity:200 * 1024 * 1024
diskPath:#"netcache"];
[NSURLCache setSharedURLCache:URLCache];
(50MB in memory and 200MB on disk)
When downloading the images (using AFNetworking) I modify the response headers to set Cache-Control to max-age=31536000 which means it should cache the response for one year.
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60.0];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Download completed
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Download failed
}];
[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
NSURLResponse *response = cachedResponse.response;
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse*)response;
NSDictionary *headers = HTTPResponse.allHeaderFields;
NSMutableDictionary *modifiedHeaders = headers.mutableCopy;
modifiedHeaders[#"Cache-Control"] = #"max-age=31536000"; // 1 year in seconds
NSHTTPURLResponse *modifiedHTTPResponse = [[NSHTTPURLResponse alloc]
initWithURL:HTTPResponse.URL
statusCode:HTTPResponse.statusCode
HTTPVersion:#"HTTP/1.1"
headerFields:modifiedHeaders];
return [[NSCachedURLResponse alloc] initWithResponse:modifiedHTTPResponse data:cachedResponse.data userInfo:cachedResponse.userInfo storagePolicy:NSURLCacheStorageAllowed];
}];
[self.operationQueue addOperation:operation];
...and yet it seems like images are released and won't even get "re-cached" when downloaded again. (I believe they are reported as cached, even though they won't load when trying to display them when the device doesn't have any connection.)
The images are later displayed using AFNetworking's UIImageView+AFNetworking.h category, like so:
[self.imageView setImageWithURL:url];
Any ideas?
I have faced the same problem. First of all you can use AFURLSessionManager's method
- (void)setDataTaskWillCacheResponseBlock:(NSCachedURLResponse * (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse))block;
to set cache block for all requests at the same time.
Second - check that 'Cache-Control' field in response headers is 'public'. For example:
[someAFHTTPSessionManager setDataTaskWillCacheResponseBlock:^NSCachedURLResponse *(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse)
{
NSHTTPURLResponse *resp = (NSHTTPURLResponse*)proposedResponse.response;
NSMutableDictionary *newHeaders = [[resp allHeaderFields] mutableCopy];
if (newHeaders[#"Cache-Control"] == nil) {
newHeaders[#"Cache-Control"] = #"public";
}
NSHTTPURLResponse *response2 = [[NSHTTPURLResponse alloc] initWithURL:resp.URL statusCode:resp.statusCode HTTPVersion:#"1.1" headerFields:newHeaders];
NSCachedURLResponse *cachedResponse2 = [[NSCachedURLResponse alloc] initWithResponse:response2
data:[proposedResponse data]
userInfo:[proposedResponse userInfo]
storagePolicy:NSURLCacheStorageAllowed];
return cachedResponse2;
}];
Third: If image's size is bigger than 5% of total cache space it will not be cached. Also it seems that there are some additional hidden rules.
I have a method to download a JSON result from a server regularly so I would like to learn how to input parameters as opposed to keep typing out the same method over and over!
Here is what I have so far:
-(void)downloadData:(NSString *)saveto downloadURL:(NSString *)URL parameters:(NSString *)params{}
It is mostly working ok, except where I am attempting to save my result. I wish to store my result in an array called "locations", I am trying to pass the name locations in the "saveto" NSString, but am not sure how to do so?
Originally I used:
locations =[[NSMutableArray alloc] init];
I would like to somehow pass the name of the array I wish to save to so like this?:
saveto = [[NSMutableArray alloc] init];
Example to run method:
[self downloadData:[NSString stringWithFormat:#"locations"] downloadURL:[NSString stringWithFormat:#"http://test.com:80/test/locations.php"] parameters:[NSString stringWithFormat:#"welcome=hi"]];
You haven't shown us how you're retrieving the data (synchronously or asynchronously?) nor how you're building that request (it's curious that params and saveto and URL are all string parameters).
But I'd suggest doing it asynchronously (since you never want to block the main queue). And in that case, you provide a "block" parameter that the caller can specify the block of code to run when the download is done.
So, you might have downloadData that looks something like:
- (void)downloadDataWithURL:(NSURL *)URL parameters:(NSDictionary *)parameters completion:(void (^)(NSArray *array, NSError *error))completion
{
// build your request using the URL and parameters however you want
NSURLRequest *request = ...;
// now issue the request asynchronously
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (!data) {
completion(nil, connectionError);
return;
}
NSError *parseError = nil;
NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];
completion(array, parseError);
}];
}
And can be invoked as:
NSURL *URL = [NSURL URLWithString:#"http://test.com:80/test/locations.php"];
NSDictionary *parameters = #{#"welcome" : #"hi"};
[self downloadDataWithURL:URL parameters:parameters completion:^(NSArray *array, NSError *error) {
if (error) {
NSLog(#"downloadDataWithURL error: %#", error);
return;
}
self.locations = array;
}];
FYI, I made the URL to be a NSURL object (to conform to common practice). I also made the parameters a NSDictionary (so that if you're making a GET request or POST request of type application/x-www-form-urlencoded, you can more easily do the necessary CFURLCreateStringByAddingPercentEscapes; if it's JSON request, this also makes it easier to make a proper JSON request).
But feel free to change the parameters URL and parameters to be whatever type makes sense for your particular implementation, but hopefully this illustrates the idea. Add an additional completion block parameter which will be the block of code that will be run when the download is done, and then the caller do whatever it wants with the results.
You should not reinvent the wheel and you should use one of the frameworks like AFNetworking or RestKit to do this stuff for you. In addition to doing the work for you it gives you very powerful tools like reachability and error handling and most importantly it handles converting to and from JSON (and many other formats). When you use AFNetworking you get an NSDictionary called responseObject that you can use in many ways including setting an NSString. Most of the time you will simply use that dictionary as the dataSource for your UI so all of the work is done for you.
AFNetworking code looks like this:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://example.com/resources.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"JSON: %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
As you can see in only a couple of lines of code you handle success, failure, and the creation of an object with all your info. No need for [[NSDictionary alloc] init]
This is one of the many many great tutorials on AFNetworking
So I'm rewriting an app for iOS 7 with AFNetworking 2.0 and I'm running into the issue of sending a batch of requests at once and tracking their progress. In the old AFNetworking there was the enqueueBatchOfHTTPRequestOperations:progressBlock:completionBlock: method on AFHTTPClient, this is clearly refactored out and I'm a bit confused on how to enqueue multiple requests.
I have created a subclass of AFHTTPSessionManager and I'm using the POST:... and GET:... methods to communicate with the server. But I can't find anything in the code and/or docs to enqueue multiple requests at once like with the old AFHTTPClient.
The only thing I can find is the undocumented batchOfRequestOperations:progressBlock:completionBlock: method on AFURLConnectionOperation, but that looks like the iOS 6 way of doing this.
Clearly I'm missing something in the new NSURLSession concept that I should use to batch requests or looking over a new AFNetworking feature. Hope someone can help me on the right track here!
tl;dr: How can I send a batch of requests with my AFHTTPSessionManager subclass?
Thanks Sendoa for the link to the GitHub issue where Mattt explains why this functionality is not working anymore. There is a clear reason why this isn't possible with the new NSURLSession structure; Tasks just aren't operations, so the old way of using dependencies or batches of operations won't work.
I've created this solution using a dispatch_group that makes it possible to batch requests using NSURLSession, here is the (pseudo-)code:
// Create a dispatch group
dispatch_group_t group = dispatch_group_create();
for (int i = 0; i < 10; i++) {
// Enter the group for each request we create
dispatch_group_enter(group);
// Fire the request
[self GET:#"endpoint.json"
parameters:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
// Leave the group as soon as the request succeeded
dispatch_group_leave(group);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
// Leave the group as soon as the request failed
dispatch_group_leave(group);
}];
}
// Here we wait for all the requests to finish
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// Do whatever you need to do when all requests are finished
});
I want to look write something that makes this easier to do and discuss with Matt if this is something (when implemented nicely) that could be merged into AFNetworking. In my opinion it would be great to do something like this with the library itself. But I have to check when I have some spare time for that.
Just updating the thread... I had the same problem and after some researches I found some good solutions, but I decided to stick with this one:
I am using the project called Bolts. So, for the same sample above posted by #Mac_Cain13, it would be:
[[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) {
BFTask *task = [BFTask taskWithResult:nil];
for (int i = 0; i < 10; i++) {
task = [task continueWithBlock:^id(BFTask *task) {
return [self executeEndPointAsync];
}];
}
return task;
}] continueWithBlock:^id(BFTask *task) {
// Everything was executed.
return nil;
}];;
- (BFTask *) executeEndPointAsync {
BFTaskCompletionSource *task = [BFTaskCompletionSource taskCompletionSource];
[self GET:#"endpoint.json" parameters:nil
success:^(NSURLSessionDataTask *task, id responseObject) {
[task setResult:responseObject];
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
[task setError:error];
}];
}];
return task.task;
}
Basically, it's stacking all of the tasks, waiting and unwrapping until there is no more tasks, and after everything is completed the last completion block is executed.
Another project that does the same thing is RXPromise, but for me the code in Bolts was more clear.
For request which can be post or get, you can use AFNetworking 2.0 for batch operation as firstly you need to create operation like this:
//Request 1
NSString *strURL = [NSString stringWithFormat:#"your url here"];
NSLog(#"scheduleurl : %#",strURL);
NSDictionary *dictParameters = your parameters here
NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] requestWithMethod:#"POST" URLString:strURL parameters:dictParameters error: nil];
AFHTTPRequestOperation *operationOne = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operationOne = [AFHTTPResponseSerializer serializer];
[operationOne setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
//do something on completion
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"%#",[error description]);
}];
//Request 2
NSString *strURL1 = [NSString stringWithFormat:#"your url here"];
NSLog(#"scheduleurl : %#",strURL);
NSDictionary *dictParameters1 = your parameters here
NSMutableURLRequest *request1 = [[AFHTTPRequestSerializer serializer] requestWithMethod:#"POST" URLString:strURL1 parameters:dictParameters1 error: nil];
AFHTTPRequestOperation *operationTwo = [[AFHTTPRequestOperation alloc] initWithRequest:request1];
operationTwo = [AFHTTPResponseSerializer serializer];
[operationTwo setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject)
{
//do something on completion
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"%#",[error description]);
}];
//Request more here if any
Now perform batch operation like this :
//Batch operation
//Add all operation here
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:#[operationOne,operationTwo] progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations)
{
NSLog(#"%i of %i complete",numberOfFinishedOperations,totalNumberOfOperations);
//set progress here
yourProgressView.progress = (float)numberOfFinishedOperations/(float)totalNumberOfOperations;
} completionBlock:^(NSArray *operations)
{
NSLog(#"All operations in batch complete");
}];
[[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO];
On AFNetworking 2.0, AFHTTPClient has been split on AFHTTPRequestOperationManager and AFHTTPSessionManager, so probably you could start with the first, which has operationQueue property.
Currently, NSURLSession tasks are not suitable for the same kind of patterns request operations use. See the answer from Mattt Thompson here regarding this issue.
Direct answer: if you need dependencies or batches, you'll still need to use request operations.