I am using the following to delete objects using RestKit (0.23.1):
[self.objectManager deleteObject:myObject path:path parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"success deleting myObject");
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"failure deleting myObject");
}];
This does create the correct DELETE request and the server returns a 200 status code. However, the local data is not deleted from Core Data. If I do a fetch for the deleted object after the success block above is fired, the fetch returns the object that should be deleted.
The documentation reads:
RKManagedObjectRequestOperation adds special behavior to DELETE
requests. Upon retrieving a successful (2xx status code) response for
a DELETE, the operation will invoke deleteObject: with the operations
targetObject on the managed object context. This will delete the
target object from the local store in conjunction the successfully
deleted remote representation.
Doing a little spelunking it looks like -[RKManagedObjectRequestOperation deleteTargetObject:] is not being called. It seems that it should be called by -[RKManagedObjectRequestOperation willFinish].
It seems like there may be a logic error in the gating if statement in -[RKManagedObjectRequestOperation willFinish].
- (void)willFinish
{
NSMutableIndexSet *deleteableStatusCodes = [NSMutableIndexSet indexSet];
[deleteableStatusCodes addIndex:404]; // Not Found
[deleteableStatusCodes addIndex:410]; // Gone
if (self.error && self.targetObjectID
&& [[[self.HTTPRequestOperation.request HTTPMethod] uppercaseString] isEqualToString:#"DELETE"]
&& [deleteableStatusCodes containsIndex:self.HTTPRequestOperation.response.statusCode]) {
NSError *error = nil;
if (! [self deleteTargetObject:&error]) {
RKLogWarning(#"Secondary error encountered while attempting to delete target object in response to 404 (Not Found) or 410 (Gone) status code: %#", error);
self.error = error;
} else {
if (! [self saveContext:&error]) {
} else {
// All good, clear any errors
self.error = nil;
}
}
}
}
It seems like -[RKManagedObjectRequestOperation willFinish] should be as follows:
- (void)willFinish
{
NSMutableIndexSet *deleteableStatusCodes = [NSMutableIndexSet indexSet];
[deleteableStatusCodes addIndex:404]; // Not Found
[deleteableStatusCodes addIndex:410]; // Gone
[deleteableStatusCodes addIndexes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
if (self.error == nil
&& self.targetObjectID
&& [[[self.HTTPRequestOperation.request HTTPMethod] uppercaseString] isEqualToString:#"DELETE"]
&& [deleteableStatusCodes containsIndex:self.HTTPRequestOperation.response.statusCode]) {
NSError *error = nil;
if (! [self deleteTargetObject:&error]) {
RKLogWarning(#"Secondary error encountered while attempting to delete target object in response to 404 (Not Found) or 410 (Gone) status code: %#", error);
self.error = error;
} else {
if (! [self saveContext:&error]) {
} else {
// All good, clear any errors
self.error = nil;
}
}
}
}
To summarize the changes, I added the successful status codes to the deleteableStatusCodes index set and check to make sure that self.error is equal to nil.
Is my logic change sound, or am I misunderstanding some RestKit convention here? It seems like my logic change aligns with the quoted statement from the documentation.
You're looking in the wrong place. See the line:
success = [weakSelf deleteTargetObject:&error];
In the performMappingOnResponseWithCompletionBlock: method in RKManagedObjectRequestOperation which checks for success status and deletes the object.
The code you have edited is to deal with error responses which, in a nice REST interface, mean the same thing as a DELETE success response.
Related
I have a situation where I will be getting more than 25000 records from web service, it is sending using pagination technique.
so the problem is I just want to store the data so for that I am thinking to run it in a loop but in future records may vary (i.e 30000,50000 etc)
from backend I am getting on each page 10000 records,but i dont know how many times i have run the loop so how do I handle this problem?
-(void)vendorsListCalling:(NSInteger)pageIndex{
[[ServicesHandler new] callVendorDetailsServiceWithParams:#{#"pageno":#(pageIndex)} CompletionBLock:^(NSDictionary *response, NSError *error) {
if (error) {
NSLog(#"error log %#",error.localizedDescription);
}else{
NSDictionary *dict = response[#"params"][#"data"];
[vendorDictionay addEntriesFromDictionary:dict];
pageCount++;
[[NSUserDefaults standardUserDefaults] setObject:vendorDictionay forKey:#"vendorsDict"];
}
}];
}
above block is where i stuck .
Any suggestions would be more appreciated.
You can store data into sqlite database. And for recursive calling for service, you can modify the same method as,
-(void)vendorsListCalling:(NSInteger)pageIndex {
if (!loader) {
//Write code to Show your loader here
}
[[ServicesHandler new] callVendorDetailsServiceWithParams:#{#"pageno":#(pageIndex)} CompletionBLock:^(NSDictionary *response, NSError *error) {
if (error) {
NSLog(#"error log %#",error.localizedDescription);
//If it fails you need to call the service again with the same Index
[self vendorsListCalling:pageCount];
} else {
if (!response[#"params"][#"data"]) {
//Stop loader since you didn't received any data
} else {
NSDictionary *dict = response[#"params"][#"data"];
[vendorDictionay addEntriesFromDictionary:dict];
pageCount++;
// Store Data in database here //
//Call service with incremented Index
[self vendorsListCalling:pageCount];
}
}
}];
}
I'm making some app and I want to provide offline functionality to it.
Problem is with getting new data from backend as temporary objects not saved in persistent store. Why I want this? Because I want to check whether data from backend is newer than offline one (by update date) If yes then update, otherwise, send it to the backend.
For now I'm trying something like this:
NSMutableURLRequest *apiEmailRequest = [[RKObjectManager sharedManager] requestWithObject:#"ApiEmail" method:RKRequestMethodGET path:pathToContent parameters:nil];
RKObjectRequestOperation *apiEmailOperation = [[RKObjectManager sharedManager] managedObjectRequestOperationWithRequest:apiEmailRequest managedObjectContext:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
*********************CHECK FOR BACKEND EMAILS AND OFFLINE ONE **********************
NSArray *backendEmails = [mappingResult array];
for (ApiEmail *backendEmail in backendEmails) {
if ([backendEmail isKindOfClass:[ApiEmail class]]) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"ApiEmail"];
NSPredicate *filterByApplication = [NSPredicate
predicateWithFormat:#"emailId == %#", backendEmail.emailId];
[fetchRequest setPredicate:filterByApplication];
NSArray *persistentEmails = [[RKManagedObjectStore defaultStore].persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil];
*HERE PUT IT INTO mainQueueManagedObjectContext and
saveToPersistentStore else POST it to the backend*
}
}
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
*ERROR*
}];
return apiEmailOperation;
[[RKObjectManager sharedManager] enqueueObjectRequestOperation:apiEmailOperation];
Is there any way to do it without creating new RKObjectManager?
Best regards, Adrian.
UPDATE
-(void)willSave {
[super willSave];
NSDictionary *remoteCommits = [NSDictionary dictionaryWithDictionary:[self committedValuesForKeys:#[#"updateDate"]]];
NSDate *updateDate = [remoteCommits valueForKey:#"updateDate"];
NSComparisonResult result = [self.updateDate compare:updateDate];
if(result == NSOrderedDescending) {
[self.managedObjectContext refreshObject:self mergeChanges:NO];
} else {
[self.managedObjectContext refreshObject:self mergeChanges:YES];
}
}
After such modification I'm getting Failed to process pending changes before save. The context is still dirty after 1000 attempts.
The below is unlikely to work in your situation actually, specifically because of the way discardsInvalidObjectsOnInsert works.
You may be able to do this by following the below process but additionally implementing willSave on the managed object and checking the status there. If you decide to abandon the updates you can try using refreshObject:mergeChanges: with a merge flag of NO.
With KVC validation you have 2 options:
edit individual pieces of data as it is imported
abandon the import for a whole object
Option 2. requires that you use the Core Data validation to prevent the import. That means doing something like making the date stamp on the object non-optional (i.e. required) and in your KVC validation setting it to nil when you determine that the import should be aborted.
Note that for this to work you need to set discardsInvalidObjectsOnInsert on the entity mapping.
After big help from #Wain, I finally got it working. Without this brave men I would still be in the sandbox. Solution:
-(BOOL)validateUpdateDate:(id *)ioValue error:(NSError **)outError {
NSComparisonResult result = [self.updateDate compare:(NSDate *)*ioValue];
if (result == NSOrderedDescending) {
self.updateDate = nil;
return NO;
}
return YES;
}
-(void)willSave {
[super willSave];
if (self.updateDate == nil) {
[self.managedObjectContext refreshObject:self mergeChanges:NO];
}
}
Thank You so much for your time and help.
Best regards, Adrian.
I have a 'JSON' data with different status codes, as shown in the image, I'm using 'AFHTTPSessionManager', And if we call the API I get 200 as success code, Apart from that in my response object I have status codes So I want to do different operations based on the status codes, For this I have written code like this..
-(void)validateOTP:(NSString *)OTP andUserID:(NSString *)userID withCompletion:(void(^)(NSDictionary* dictionary, NSError* error))completion
{
UTValidateOTPRequest *request = [UTValidateOTPRequest validateOTPRequestWithOTP:OTP andUserID:userID];
NSDictionary *paramDict = [request dictionaryRepresentation];
NSURLSessionDataTask *task = [self postRequestToResouce:[NSString stringWithFormat:#"%#/ValidateOTP",kServerCommonEndPoint] arguments:paramDict successHandler:^(NSDictionary *response) {
NSNumber *statusCode = response[kStatusCodeKey];
if (statusCode.integerValue == UTStatusCodeSuccess) {
completion(response, nil);
}
else {
NSError *error = [NSError mapStatusCodeToError:statusCode.integerValue details:response];
completion(nil, error);
}
} errorHandler:^(NSError *error) {
NSError *newError = [NSError mapStatusCodeToError:error.code details:error.userInfo];
completion(nil, newError);
}];
[self addDataTask:task];
}
As you can see, even inside success handler I'm passing error and calling a category method we have created.
NSError *error = [NSError mapStatusCodeToError:statusCode.integerValue details:response];
This method implemented as follows
+ (NSError *)mapStatusCodeToError: (NSInteger)statusCode details:(NSDictionary*) errorInfo
{
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *domain = [bundleIdentifier stringByAppendingString:kErrorDomainKey];
NSString *errorMessage = nil;
if (errorInfo[kErrorMessageKey] && ![errorInfo[kErrorMessageKey] isEqualToString:kEmptyString]) {
errorMessage = errorInfo[kErrorMessageKey];
}
else{
// use common message
errorMessage = kInternetNotAvailableMessage;
}
NSDictionary *userInfo = #{NSLocalizedDescriptionKey:NSLocalizedString(errorMessage, nil)};
NSLog(#"User INFO : %#",userInfo);
NSError *internetUnavailableError = [NSError errorWithDomain:domain
code:NSURLErrorNotConnectedToInternet
userInfo:userInfo];
NSLog(#"Error Code : %ld",(long)internetUnavailableError.code);
return internetUnavailableError;
}
Here I want to use the statusCode that I'm passing as parameter to this method so that I can get that status code where I'm calling this method
[[UTServerManager sharedInstance] validateOTP:self.enterOTPText.text andUserID:self.userId withCompletion:^(NSDictionary *dictionary, NSError *error) {
// Here I want to get the status code of error not like -1009 , but what ever statusCode that I'm getting from the API response.
}];
So in this method can I get the response status code if it is not success code means as you see in the image in the first response is success and remaining are error responses.
And I know that I can use the statusCode in the category method but I dont know how to use it, If I store this status-Code in the category method for error as above , that I'm passing as a parameter to the methods then I can call them where ever I require How to get this ?
And my restriction is I should these methods only but I have to add the response status code to error ??
Looks like you are passing a userInfo dict to the NSError. You could add to this dictionary prior to creating the NSError you are returning, and then later it would be available in the NSError callback.
I'm getting stuck with an error when using my Watchkit Application. When I launch it, I ask the containing iOS app to get some data from network. The problem is that I get an error saying the containing app never calls 'reply()' :o But looking at my code, it should call it.
I tried to debug every step from openParentApplication to the 'reply()' call, and it seems to work well =X
Here is my code in the Watchkit extension
- (void)initDiaporamasWithSuccess:(void (^)())success andFailure:(void (^)(NSError*))failure {
NSLog(#"Ask to load diapos");
__weak typeof(self) weakSelf = self;
[WKInterfaceController openParentApplication:#{#"watchKit": #"watchKit.initDiapos"} reply:^(NSDictionary *replyInfo, NSError *error) {
if (error) {
NSLog(#"%#", error);
if (failure) {
failure(error);
}
return;
}
NSLog(#"got items : %#", replyInfo[#"diapos"]);
weakSelf.diaporamas = replyInfo[#"diapos"];
[weakSelf setDiaporama:replyInfo[#"firstDiapo"] AtIndex:0];
if (success) {
success();
}
}];
}
The result should be an NSDictionary containing an NSArray with some diaporamas basic informations, and an object (Diapo) containing the full informations of the first diaporama (e.g. self.diaporamas[0])
And here is the code in the containing app's AppDelegate :
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply {
// Maybe we could handle multiple watchKit extension calls that way ?
// Something like a key-value 'protocol' to run the right block of code
NSString *watchKitCall = userInfo[#"watchKit"];
NSLog(#"watchKit handled");
if ([watchKitCall isEqualToString:#"watchKit.initDiapos"]) {
[AppDelegate watchInitialObjects:^(NSDictionary *info) {
NSLog(#"Managed to get initial infos");
reply(info);
} failure:^(NSError *error) {
NSLog(#"Fail : %#", error);
reply(#{#"error": error});
}];
}
}
+ (void) watchInitialObjects:(void (^)(NSDictionary *info))success failure:(void (^)(NSError *error))failure {
NSDictionary *parameters = #{#"site" : #(14), #"limit" : #(10)};
[AppDelegate requestDiapoListWithParams:parameters success:^(NSArray *items) {
if ([items count] == 0)
{
NSError *error = [NSError errorWithDomain:#"com.domain.app" code:404 userInfo:nil];
failure(error);
return;
}
Diapo *firstDiapo = [items firstObject];
[AppDelegate requestDiapoDetailWithDiapo:firstDiapo success:^(Diapo *diapo) {
if (!diapo)
{
NSError *error = [NSError errorWithDomain:#"com.domain.app" code:404 userInfo:nil];
failure(error);
return;
}
NSDictionary *result = #{
#"firstDiapo" : diapo,
#"diapos" : items
};
success(result);
} failure:^(NSError *error) {
failure(error);
}];
} failure:^(NSError *error) {
failure(error);
}];
}
In the watchKitHandler, I call watchInitialObjects to get the diaporamas array and the first diaporama's informations.
In the watchInitialObjects, I make a first network call to get the array, and on success, I make an other network call to get the firs diaporama informations.
To make the calls and map the JSON into objects, I use RESTKit
I really don't get what could be the error =x
UPDATE
I forgot to write the error I get, here it is :
Error Domain=com.apple.watchkit.errors Code=2 "The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]" UserInfo=0x7fcb53e12830 {NSLocalizedDescription=The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]}
And I kept trying to know why I get this error, and I think I found it:
It seems that there is a (very little) timeout to do the work in the containing app. But I mapped the JSON data I received directly in the containing app and then, send those custom objects in the reply(). But when I removed the mapping part, it worked well !
So...that's why I think that was the problem =X
Does anybody could approve my thoughts or corrects me ?
After hours of searching and testing different codes, I finally found my problem...and it's obvious when we read the Apple documentation about 'application:handleWatchKitExtensionRequest:reply:' seriously...
here is the answer : (it's in the documentation)
The contents of the dictionary must be serializable to a property list file.
Which means that objects can ONLY be dictionaries, arrays, strings, numbers (integer and float), dates, binary data, or Boolean values
...I feel dumb ><
If I have an array of Message objects, each with a PFile containing data, is it possible to download the data for every single message by queuing them up asynchronously like so:
for (int i = 0; i < _downloadedMessages.count; i++) {
PFObject *tempMessage = (PFObject *)[_downloadedMessages objectAtIndex:i];
[[tempMessage objectForKey:#"audio"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
[self persistNewMessageWithData:data];
}];
}
This seems to cause my app to hang, even though this should be done in the background...
Using the solution below:
NSMutableArray* Objects = ...
[self forEachPFFileInArray:Objects retrieveDataWithCompletion:^BOOL(NSData* data, NSError*error){
if (data) {
PFObject *tempObj = (PFObject *)Object[someIndex...];
[self persistNewMessageWithData:data andOtherInformationFromObject:tempObj];
return YES;
}
else {
NSLog(#"Error: %#", error);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];
What you are possibly experiencing is, that for a large numbers of asynchronous requests which run concurrently, the system can choke due to memory pressure and due to network stalls or other accesses of resources that get exhausted (including CPU).
You can verify the occurrence of memory pressure using Instruments with the "Allocations" tool.
Internally (that is, in the Parse library and the system) there might be a variable set which sets the maximum number of network requests which can run concurrently. Nonetheless, in your for loop you enqueue ALL requests.
Depending of what enqueuing a request means in your case, this procedure isn't free at all. It may cost a significant amount of memory. In the worst case, the network request will be enqueued by the system, but the underlying network stack executes only a maximum number of concurrent requests. The other enqueued but pending requests hang there and wait for execution, while their network timeout is already running. This may lead to cancellation of pending events, since their timeout expired.
The simplest Solution
Well, the most obvious approach solving the above issues would be one which simply serializes all tasks. That is, it only starts the next asynchronous task when the previous has been finished (including the code in your completion handler). One can accomplish this using an asynchronous pattern which I name "asynchronous loop":
The "asynchronous loop" is asynchronous, and thus has a completion handler, which gets called when all iterations are finished.
typedef void (^loop_completion_handler_t)(id result);
typedef BOOL (^task_completion_t)(PFObject* object, NSData* data, NSError* error);
- (void) forEachObjectInArray:(NSMutableArray*) array
retrieveDataWithCompletion:(task_completion_t)taskCompletionHandler
completion:(loop_completion_handler_t)completionHandler
{
// first, check termination condition:
if ([array count] == 0) {
if (completionHandler) {
completionHandler(#"Finished");
}
return;
}
// handle current item:
PFObject* object = array[0];
[array removeObjectAtIndex:0];
PFFile* file = [object objectForKey:#"audio"];
if (file==nil) {
if (taskCompletionHandler) {
NSDictionary* userInfo = #{NSLocalizedFailureReasonErrorKey: #"file object is nil"}
NSError* error = [[NSError alloc] initWithDomain:#"RetrieveObject"
code:-1
userInfo:userInfo];
if (taskCompletionHandler(object, nil, error)) {
// dispatch asynchronously, thus invoking itself is not a recursion
dispatch_async(dispatch_get_global(0,0), ^{
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
});
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}
}
else {
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
BOOL doContinue = YES;
if (taskCompletionHandler) {
doContinue = taskCompletionHandler(object, data, error);
}
if (doContinue) {
// invoke itself (note this is not a recursion")
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}];
}
}
Usage:
// Create a mutable array
NSMutableArray* objects = [_downloadedMessages mutableCopy];
[self forEachObjectInArray:objects
retrieveDataWithCompletion:^BOOL(PFObject* object, NSData* data, NSError* error){
if (error == nil) {
[self persistNewMessageWithData:data andOtherInformationFromObject:object];
return YES;
}
else {
NSLog(#"Error %#\nfor PFObject %# with data: %#", error, object, data);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];