I am trying to figure out how long my round trip to the server is before any mapping occurs. I need to get at the RKObjectRequestOperation, but it is only available in the success and fail blocks.
I see that RestKit 2 does send a notification:
[[NSNotificationCenter defaultCenter] postNotificationName:RKObjectRequestOperationDidStartNotification object:weakSelf];
But there is no user info sent along.
Any ideas on how I can do this? I was thinking of an associated object onto the operation queue but that is causing crashes.
What I did:
self.op = self.objectManager.operationQueue.operations.lastObject;
objc_setAssociatedObject(self, &kRetrieverRequestOperationKey, self.op, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
Added the above right after:
[self.objectManager getObjectsAtPath:resourcePath parameters:parmsDictionary success:^(RKObjectRequestOperation *requestOperation, RKMappingResult *mappingResult)
{
[weakSelf didLoadOperation:requestOperation result:mappingResult isFromCache:NO];
[weakSelf requestDidEnd:requestOperation];
} failure:^(RKObjectRequestOperation *requestOperation, NSError *error) {
[weakSelf requestOperation:requestOperation didFailWithError:error];
[weakSelf requestDidEnd:requestOperation];
}];
Then when RestKit posted its notification I was able to get at the RKObjectRequestOperation.
Not ideal, but seems to work.
Related
[[RKObjectManager sharedManager] getObjectsAtPath:#"/mypath/objects" parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
// Use objects that are returned.
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
// Handle error
}];
This method of RestKit will perform a request, mapping, saving to core data, and performing the success callback AFTER saving. What I want to know is the method I can use to get the response and perform a callback BEFORE RestKit automatically saves it in core data. I still want to be able to use the ability of RestKit to do the mapping and saving but I want to intercept or at least get a callback BEFORE it performs saving of data.
I found a way to do it by implementing the callback block of the RKManagedObjectRequestOperation before the mapping begins since this will be called after a successful HTTP response and before the automatic saving to core data:
[managedObjectRequestOperation setWillMapDeserializedResponseBlock:^id(id deserializedResponseBody) {
//do something
return deserializedResponseBody;
}];
Is there a way to differ between mapping results which are empty due to there being no data do map att the key path and a mapping result which is empty due do skipping objects through KVC.
The problem I have is that I am working with a API which is not very well thought through, and I need to find a way to know when I am at the end of the page. The issue is that there is a possibility that the mapping result can be empty due to all the objects being returned are "deleted".
TLDR; Is there a way to see the amount of mapping errors through mapping result on a completed request.
EDIT
I solved my own problem by reading the documentation again. The solution was to create a subclass of RKObjectRequestOperation and then register it.
[[RKObjectManager sharedManager] registerRequestOperationClass:[CustomRequestOperation class]];
This mean that I could implement the following which would listen to mapping errors and increment a counter and add it to mappingMetaData when the mapping completed.
- (void)setCompletionBlockWithSuccess:(void ( ^ ) ( RKObjectRequestOperation *operation , RKMappingResult *mappingResult ))success failure:(void (^) ( RKObjectRequestOperation *operation , NSError *error ))failure {
__block typeof(skipCount) temp = skipCount;
[super setCompletionBlockWithSuccess:^void(RKObjectRequestOperation *operation , RKMappingResult *mappingResult) {
if (success) {
operation.mappingMetadata = #{#"skipCount" : #(temp)};
success(operation, mappingResult);
}
} failure:^void(RKObjectRequestOperation *operation , NSError *error) {
if (failure) {
failure(operation, error);
}
}];
}
- (void)mapper:(RKMapperOperation *)mapper didFailMappingOperation:(RKMappingOperation *)mappingOperation forKeyPath:(NSString *)keyPath withError:(NSError *)error {
skipCount++;
}
- (void)mapperWillStartMapping:(RKMapperOperation *)mapper {
skipCount = 0;
}
- (void)loadItems {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
[manager GET:#"someurl"
parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self reloadData];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems];
}
I am trying to implement instant search by making an API call every time a character changes. Since, the first few calls have less letters, they return more results, making the first few async calls finish slower than than the last few, meaning that if I type in hello quickly, I will end up getting the search results for h instead of the whole word since the last call to finish is the one for h. I need to keep the order of these calls, and make sure that the last query is not overwritten. I understand that I must use a queue structure. However doing something like this in textFieldDidChange doesn't seem to work:
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[self loadItems];
});
dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[self reloadData];
});
I think I need to use some sort of combination of dispatch_group_enter(group); and dispatch_group_leave(group);. However I still can't get the calls to stop overwriting the last call. I'm not sure if there is also a way to just cancel out all the other started calls with the last one, or if I have to wait for all of them to finish in order. Any help would be appreciated.
This was my solution. I just ended up using a counter that I pass into my loadItems function. While that counter updates, the async call still has its own value in it, so I just compare the two, and make sure to only reloadData if the async call's counter is equal to the latest one.
- (void)loadItems:(int)queryInt {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
[manager GET:#"someurl"
parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if (searchQueryCounter - 1 == queryInt) {
[self reloadDatawithAnimation];
} else {
return;
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems:searchQueryCounter];
searchQueryCounter = searchQueryCounter + 1;
}
You might better address this by canceling the prior requests, not only preventing prior requests reporting results, but also ensuring that system resources are not consumed by requests that are no longer needed:
#interface ViewController () <UITextFieldDelegate>
#property (nonatomic, strong) AFHTTPRequestOperationManager *manager;
#property (nonatomic, weak) NSOperation *previousOperation;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// no need to instantiate new request operation manager each time;
// do it at some logical point of initialization (e.g. in `viewDidLoad`
// for view controllers, etc.).
self.manager = [AFHTTPRequestOperationManager manager];
[self.manager.requestSerializer setValue:#"text/html" forHTTPHeaderField:#"Content-Type"];
}
- (void)loadItems {
[self.previousOperation cancel];
typeof(self) __weak weakSelf = self; // probably should use weakSelf pattern, too
NSOperation *operation = [self.manager GET:#"someurl" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[weakSelf reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if ([error.domain isEqualToString:NSURLErrorDomain] && [error code] != NSURLErrorCancelled) {
NSLog(#"Error: %#", error);
}
}];
self.previousOperation = operation;
}
- (void)textFieldDidChange {
[_filteredArray removeAllObjects];
[self loadItems];
}
#end
I actually worked on a very similar problem last week and came up with an approach you might find useful.
I submit each request using performSelector:withObject:afterDelay with a slight delay (I've been experimenting with values ranging from 0.5 to 1.0 seconds. Somewhere from .66 to .75 seems like a good compromise value.)
With each new request, I cancel the previous pending performSelector call. That way nothing gets sent until the user stops typing for a short period of time. It's not perfect, but it reduces the amount of useless queries for word fragments. The code looks something like this:
static NSString *methodWord = nil;
[[self class] cancelPreviousPerformRequestsWithTarget: self
selector: #selector(handleWordEntered:)
object: methodWord];
methodWord = word;
[self performSelector: #selector(handleWordEntered:)
withObject: methodWord
afterDelay: .667];
The method handleWordEntered: actually sends the request to the server.
If the user types a letter, then another letter in less than 2/3 second, the previous pending request is cancelled and a new request is set to fire 2/3 of a second later. As long as the user keeps typing letters every 2/3 second, nothing is sent. As soon as the user pauses more than 2/3 second, a request is sent. Once the performSelector:withObject:afterDelay fires it can't be cancelled any more, so that request goes to the network and the reply is parsed.
Using Restkit, I've setup the RKObjectManager in my AppDelegate and everything is working fine. I would like to know if there's some way to setup a default action for specific response codes.
For example, a user uses my iPhone app to login to my api and gets an auth_token back to use. If at any point, for any request, I get back I get a 403 response (like if the auth_token expires) I want to change the RootViewController to my login screen.
What would be the best way to set this up in my app?
In RestKit 0.20 you can register your RKObjectRequestOperation, so you can pass all requests/responses through your own success/failure blocks before anything else.
http://blog.higgsboson.tk/2013/09/03/global-request-management-with-restkit/
#import "RKObjectRequestOperation.h"
#interface CustomRKObjectRequestOperation : RKObjectRequestOperation
#end
#implementation CustomRKObjectRequestOperation
- (void)setCompletionBlockWithSuccess:(void ( ^ ) ( RKObjectRequestOperation *operation , RKMappingResult *mappingResult ))success failure:(void ( ^ ) ( RKObjectRequestOperation *operation , NSError *error ))failure
{
[super setCompletionBlockWithSuccess:^void(RKObjectRequestOperation *operation , RKMappingResult *mappingResult) {
if (success) {
success(operation, mappingResult);
}
}failure:^void(RKObjectRequestOperation *operation , NSError *error) {
[[NSNotificationCenter defaultCenter] postNotificationName:#"connectionFailure" object:operation];
if (failure) {
failure(operation, error);
}
}];
}
#end
Then register your subclass:
[[RKObjectManager sharedManager] registerRequestOperationClass:[CustomRKObjectRequestOperation class]];
And listen for the "connectionFailure" you are sending above:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(connectionFailedWithOperation:) name:#"connectionFailure" object:nil];
In the listener (e.g. your AppDelegate or a login manager):
- (void)connectionFailedWithOperation:(NSNotification *)notification
{
RKObjectRequestOperation *operation = (RKObjectRequestOperation *)notification.object;
if (operation) {
NSInteger statusCode = operation.HTTPRequestOperation.response.statusCode;
switch (statusCode) {
case 0: // No internet connection
{
}
break;
case 401: // not authenticated
{
}
break;
default:
{
}
break;
}
}
}
When using RestKit 0.10 you can use the given delegate method objectLoaderDidLoadUnexpectedResponse.
- (void)objectLoaderDidLoadUnexpectedResponse:(RKObjectLoader *)objectLoader {
if ([[objectLoader response] statusCode] == 403) {
// Your action here
}
}
In RestKit 0.20 you can use a response descriptor for a single code or a set of codes.
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:responseMapping
pathPattern:nil
keyPath:#"yourKeyPath"
statusCodes:[NSIndexSet indexSetWithIndex:403]];
More status code sets in the documentation.
Update
When using your BaseViewController for handling the errors of the request made in one of the other view controllers, you can set up notifications.
BaseViewController
- (void)viewDidLoad
{
// ...
// Set observer for notification e.g. "requestFailedWith403Error"
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handle403Error:) name:#"requestFailedWith403Error" object:self];
}
- (void)handle403Error:(NSNotification)notification
{
// Code for handling the error
}
SubViewController
- (void)loginToServer
{
// ...
// Set authorization header
[[RKObjectManager sharedManager].HTTPClient setAuthorizationHeaderWithUsername:#"username" password:#"password"];
// e.g. POST to server
[[RKObjectManager sharedManager] postObject:yourObject
path:#"path/toserver"
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
// Handling success
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
// Handling error with notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"requestFailedWith403Error" object:self];
}];
}
Too optimize your central configuration with the handling of errors you can have another look at the example code given in the RestKit Wiki (where the error mapping is added).
I am attempting to load a collection of objects via RestKit and have found that this operation is blocking the main thread. Specifically, this functionality is called when a UIViewController appears in the -(void)viewDidAppear:(BOOL)animated; message. As long as request is loading, the user is unable to interact with the UI, and has to wait until the operation has completed.
How can I instruct RestKit to execute the request asynchronously (I thought it already was) and stop blocking the main thread?
-(void)rtrvArtistsByStudio:(ISStudio *)studio
{
NSLog(#"Retrieving Artists for studio %ld", [studio studioID]);
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
NSDictionary *params = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:#"%ld", [studio studioID]] forKey:#"studioID"];
NSURLRequest *request = [restkit requestWithObject:nil method:RKRequestMethodGET path:#"/restAPI/rtrvArtistsByStudio" parameters:params];
RKManagedObjectRequestOperation *operation = [restkit managedObjectRequestOperationWithRequest:request managedObjectContext:restkit.managedObjectStore.persistentStoreManagedObjectContext success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSArray *artists = [mappingResult array];
[studio setArtists:artists];
[notificationCenter postNotificationName:nStudioLoadedArtists object:studio];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Failed to load artists for studio %ld due to %#", [studio studioID], error);
[notificationCenter postNotificationName:nStudioFailedLoadingArtists object:studio];
}];
[restkit enqueueObjectRequestOperation:operation];
}
EDIT:
I should also note that restkit is a pointer to an instance of RKObjectManager.
RestKit version is 0.20
The request is definitely being dispatched asynchronously. If you press the pause button in the Xcode UI and look at the threads, what is happening on the main thread and the background threads during the request? That should provide insight into what is causing your thread blockage.
Turns out that it was actually my own fault (of course). I have a UIViewController category that performs some transitions. During the transition I have the UIApplication ignore touch events until the transition has completed (0.2s, very brief). I completely forgot that this existed.
Because I am loading some remote objects in viewDidAppear it was still ignoring touch events until that completed. Removing the touch ignore fixed the problem.