- (BOOL)do_a_Restkit_request_and_return_a_boolean
{
[manager postObject:nil path:#"/mypath" parameters:#{#"password":password} success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
myResult = [mappingResult firstObject] == 5;
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
}];
return myResult;
}
Hello I would like to make a RestKit call like the above Synchronous so as to return myResult after the call of the Success block.
You can use an approach like this:
NSMutableURLRequest *request = // create a request…
RKObjectRequestOperation *operation = [manager objectRequestOperationWithRequest:request success:nil failure:nil];
[operation start];
[operation waitUntilFinished];
BOOL myResult = NO;
if (!operation.error) {
myResult = [operation.mappingResult firstObject] == 5;
}
return myResult;
Notes:
The completion blocks are called after the operation is finished, so if you provide success/failure blocks, they won't be called until after the method returns
(This is why I suggest you pass nil into the completion blocks.)
waitUntilFinished will block whatever thread you're on, so make sure it's not the main thread.
If you need help creating request, see Creating Request Objects in the RKObjectManager Class Reference.
Again, if you can rewrite your code to work asynchronously, that will probably be better than this solution.
You need to either embrace the asynchronous nature of network communication and background processing of the response, or use a different API / technology specifically for making synchronous requests. RestKit and AFNetworking are based more around the former.
Embracing asynchronous is the best option...
That said, you could use RestKit to create the NSURLRequest, then use NSURLConnection to synchronously download the response, then use RKMapperOperation to perform the mapping (and wait for completion with addOperations:waitUntilFinished:).
Related
I've got subclass of AFHTTPClient
The main idea is that i call all API through my singleton of AFHTTPClient subclass, and all requests goes through 1 points for error handling and HUD displaying.
This is entry point for every API calls:
-(void) makeRequestWithPath:(NSString*) path andParams:(NSDictionary*) params
success:(void (^)( id JSON, AFHTTPRequestOperation *operation)) success
failure:(void (^)( NSError *error)) failure
And i've got many methods for API calls something like that:
-(void) getListMainTreeWithSuccess:(void (^)( id JSON, AFHTTPRequestOperation *operation)) success
failure:(void (^)( NSError *error)) failure
{
[self makeRequestWithPath:#"objects/selectlist" andParams:nil success:^(id JSON, AFHTTPRequestOperation *operation) {
success(JSON,operation);
} failure:^(NSError *error) {
failure(error);
}];
}
This works just fine for my needs. But i faced problem that i need to make serial request in loop through my AFHTTPClient subclass and make some action when all of them are finished , I found method
-(void)enqueueBatchOfHTTPRequestOperationsWithRequests:(NSArray *)urlRequests
progressBlock:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progressBlock
completionBlock:(void (^)(NSArray *operations))completionBlock
which should solve my issue, but the problem is that i call all methods through AFHTTPClient and it's methods getPath: and postPath: and previous way forces me to rewrite everything and makes my subclass completely useless, because I need to add there NSArray of AFHTTPRequestoperation, which is not possible to construct or extract from my subclass and my methods. Previously i tried to use __block 's to synchronise requests with semaphore and something else but i failed to get what i need, please help me!
UPDATE:
It seems that it is not possible to even use enqueueBatchOfHTTPRequestOperations method (even with rewriting all my code) because this method needs array of http request operations, but it's not possible to construct POST request with them.
I solved this with an increment/decrement pending download system and tied the HUD to that.
[networkStatus beginNetworkActivity];
[client someRESTActionWithCompletion:^(id object, NSError *error) {
[networkStatus endNetworkActivity];
if (error) {
// Handle the error ...
}
if (![networkStatus hasNetworkActivity]) {
// All downloads have finished
}
}];
I keep the network status object separate which from the AFHTTPClient subclass, but it can be built into the client if that's what you want.
Network status keeps an internal counter. -beginNetworkActivity increments the counter, if the counter was 0, then it displays a HUD. -endNetworkActivity decrements the counter, if the counter becomes 0, then it dismisses the HUD. -hasNetworkActivity returns YES if the counter greater than 0.
Other Notes: I combine the success and failed callbacks into a single completion callback. I keep the network status logic separate from the client because sometime I'll use a singleton network status object, sometimes I'll use a created instance, sometimes I won't use one at all. It all depends on the needs to the higher level logic.
Again, as #MikePollard said, create AFHTTPRequestOperation using
[AFHHTPClient HTTPRequestOperationWithRequest:success:failure:]
For this method create NSURLRequest using (or use another one, pick which one is suitable for you). Here you can also specify, which method to use POST, GET or any other.
[AFHTTPClient requestWithMethod:
path:
parameters:]
After that save all operation to an NSArray, and schedule them using:
[AFHTTPClient enqueueBatchOfHTTPRequestOperationsWithRequests:
progressBlock:
completionBlock:]
Code example:
NSMutableArray *ops = [NSMutableArray new];
NSMutableURLRequest *request1 = [[AFHTTPClient sharedClient] requestWithMethod:#"GET"
path:#"MyEndpoint"
parameters:#{#"key1": #"value"}];
AFHTTPRequestOperation *op1 = [[AFHTTPClient sharedClient] HTTPRequestOperationWithRequest:request1
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success!");
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failure!");
}];
[ops addObject:op1];
NSMutableURLRequest *request2 = [[AFHTTPClient sharedClient] requestWithMethod:#"POST"
path:#"MyAnotherEndpoint"
parameters:#{#"key2": #(104)}];
AFHTTPRequestOperation *op2 = [[AFHTTPClient sharedClient] HTTPRequestOperationWithRequest:request2
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Success!");
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failure!");
}];
[ops addObject:op2];
[[AFHTTPClient sharedClient] enqueueBatchOfHTTPRequestOperationsWithRequests:ops
progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
NSLog(#"numberOfFinishedOperations: %d totalNumberOfOperations %d",
numberOfFinishedOperations,
totalNumberOfOperations);
}
completionBlock:^(NSArray *operations) {
NSLog(#"All operation compelted!");
}];
Say I have this method that given a URL returns a UIImage:
- (void)getUIImageFromURL:(NSURL *)URL {
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
AFHTTPRequestOperation *imageOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
imageOperation.responseSerializer = [AFImageResponseSerializer serializer];
[imageOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
return (UIImage *)responseObject;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
[imageOperation start];
}
But it keeps giving me this error:
Incompatible block pointer types sending 'UIImage *(^)(AFHTTPRequestOperation *__strong, _strong id)' to parameter of type 'void (^)(AFHTTPRequestOperation *_strong, __strong id)'
I'm somewhat new to blocks, so perhaps I'm approaching this completely backwards. How best would I implement a method like this?
You cannot return an image from inside a block. This is an asynchronous API and cannot be used in the manner you are attempting to. Either use a blocking API, where the method is blocking until the image is downloaded (a bad solution), or implement support for the asynchronous API. For instance, pass a completion block to your getImage method and call it with in the completion block of the download operation. In this block, do what you need with the image.
The callback block in setCompletionBlockWithSuccess: is a void block, you can't change it's return type.
In your case, you would probably set the image inside of your block, instead of returning an image.
[imageOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
self.myImage.image = [UIImage imageWithData:responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
However, if you are dealing with AFNetworking and images, there are category methods that should greatly simplify retrieval and cacheing.
[self.myImage setImagewithURL:URL];
You are calling an asynchronous method with two blocks for handling success and failure. By the time one of these handlers is called, your calling method is long gone. It doesn't make any sense to think you could return data to it, because it is gone.
In the success block and failure block, you give instructions what to do when the operation has succeeded or failed. There is nobody there to return anything to. What you do is add code in the block to process and store the result of success in the right place, or to handle errors in the correct way.
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.
I am doing a lot of URL requests (about 60 small images) and I have started to do them Asynchronously. My code adds another image (little downloading thing) and then sets a Request going.
When the request is done I want "data" to be put in the location which was originally added for it, however, I can not see how to pass "imageLocation" to the block for it to store the image in the correct location.
I have replaced the 3rd line with below which seems to work but I am not 100% it is correct (it is very hard to tell as the images are nearly identical). I am also thinking that it is possible to pass "imageLocation" at the point where the block is declared.
Can any confirm any of this?
__block int imageLocation = [allImages count] - 1;
// Add another image to MArray
[allImages addObject:[UIImage imageNamed:#"downloading.png"]];
imageLocation = [allImages count] - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil && error == nil)
{
//All Worked
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageWithData:data]];
}
else
{
// There was an error, alert the user
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageNamed:#"error.png"]];
}];
Dealing with asynchronous methods is a pain ;)
In your case its guaranteed that the completion block will execute on the specified queue. However, you need to ensure that the queue has a max concurrent operations count of 1, otherwise concurrent access to shared resources is not safe. That's a classic race http://en.wikipedia.org/wiki/Race_condition. The max concurrent operations of a NSOperationQueue can be set with a property.
In general, completion handlers may execute on any thread, unless otherwise specified.
Dealing with asynchronous methods gets a lot easier when using a concept called "Promises" http://en.wikipedia.org/wiki/Promise_(programming). Basically, "Promises" represent a result that will be evaluated in the future - nonetheless the promise itself is immediately available. Similar concepts are named "futures" or "deferred".
There is an implementation of a promise in Objective-C on GitHub: RXPromise. When using it you also get safe access from within the handler blocks to shared resources. An implementation would look as follows:
-(RXPromise*) fetchImageFromURL:(NSString*)urlString queue:(NSOperationQueue*) queue
{
#autoreleasepool {
RXPromise* promise = [[RXPromise alloc] init];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil) {
[promise fulfillWithValue:data];
}
else { // There was an error
[promise rejectWithReason:error];
};
}];
return promise;
}
}
Then call it:
- (void) fetchImages {
...
for (NSUInteger index = 0; index < N; ++index)
{
NSString* urlString = ...
[self fetchImageFromURL:urlString, self.queue]
.then(^id(id data){
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageWithData:data]];
return #"OK";
},
^id(NSError* error) {
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageNamed:#"error.png"]];
return error;
});
}
}
A couple of thoughts:
If you want to download 60 images, I would not advise using a serial queue for the download (e.g. do not use an operation queue with maxConcurrentOperationCount of 1), but rather use a concurrent queue. You will want to synchronize the updates to make sure your code is thread-safe (and this is easily done by dispatching the updates to a serial queue, such as the main queue, for that final update of the model), but I wouldn't suggest using a serial queue for the download itself, as that will be much slower.
If you want to use the NSURLConnection convenience methods, I'd suggest something like the following concurrent operation request approach (where, because it's on a background queue, I'm using sendSynchronousRequest rather than sendAsynchronousRequest), where I'll assume you have an NSArray, imageURLs, of NSURL objects for the URLs of your images:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:&response error:&error];
if (!data) {
NSLog(#"%s sendSynchronousRequest error: %#", __FUNCTION__, error);
} else {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self.allImages replaceObjectAtIndex:idx withObject:image];
});
}
}
}];
[queue addOperation:operation];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
A couple of asides: First, I'm using an operation queue rather than a GCD concurrent queue, because it's important to be able to constrain the degree on concurrency. Second, I've added a completion operation, because I assume it would be useful to know when all the downloads are done, but if you don't need that, the code is obviously simper. Third, that benchmarking code using CFAbsoluteTime is unnecessary, but useful solely for diagnostic purposes if you want to compare the performance using a maxConcurrentOperationCount of 4 versus 1.
Better than using the NSURLConnection convenience methods, above, you might want to use a NSOperation-based network request. You can write your own, or better, use a proven solution, like AFNetworking. That might look like:
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFImageResponseSerializer serializer];
manager.operationQueue.maxConcurrentOperationCount = 4;
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [manager GET:[url absoluteString] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self.allImages replaceObjectAtIndex:idx withObject:responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%s image request error: %#", __FUNCTION__, error);
}];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
Because AFNetworking dispatches those completion blocks back to the main queue, that solves the synchronization issues, while still enjoying the concurrent network requests.
But the main take-home message here is that an NSOperation-based network request (or at least one that uses NSURLConnectionDataDelegatemethods) opens additional opportunities (e.g. you can cancel all of those network requests if you have to, you can get progress updates, etc.).
Frankly, having walked through two solutions that illustrate how to download the images efficiently up-front, I feel compelled to point out that this is an inherently inefficient process. I might suggest a "lazy" image loading process, that requests the images asynchronously as they're needed, in a just-in-time (a.k.a. "lazy") manner. The easiest solution for this is to use a UIImageView category, such as provided by AFNetworking or SDWebImage. (I'd use AFNetworking's if you're using AFNetworking already for other purposes, but I think that SDWebImage's UIImageView category is a little stronger.) These not only seamlessly load the images asynchronously, but offer a host of other advantages such as cacheing, more efficient memory usage, etc. And, it's as simple as:
[imageView setImageWithURL:url placeholder:[UIImage imageNamed:#"placeholder"]];
Just a few thoughts on efficiently performing network requests. I hope that helps.
I am using Restkit in my iOS app to make a GET call to my server. I am able to get the call to work just fine, except that it is supposed to be asynchronous and it is blocking my main thread. I am basically using their exact sample to make the request from their github page which is as follows:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://mywebapi.com/Article"]];
RKObjectRequestOperation *operation = [[RKObjectRequestOperation alloc] initWithRequest:request responseDescriptors:#[responseDescriptor]];
[operation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *result) {
Article *article = [result firstObject];
NSLog(#"Mapped the article: %#", article);
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Failed with error: %#", [error localizedDescription]);
}];
[operation start];
This blocks all main thread executions (such as my transitions) until the success block is called. Wrapping this call inside of a dispatch_queue does resolve the issue, but it is my understanding that this method is supposed to be asynchronous on its own.
Am I missing some configuration on the RKObjectRequestOperation, or is there a different method I should be calling for an async call?
I found the answer to this question here:
RestKit Makes UI Unresponsive
Basically, you do need to add it to an NSOperationQueue, otherwise it will run the call on the main thread.