RestKit on iOS - subsequent API calls for nested objects? - ios

I'm using RestKit to pull data from the Foursquare API into my iphone app, but having trouble with the followup API call for a nested object.
Specifically:
I call the Venues search API (https://developer.foursquare.com/docs/venues/search) to retrieve a list of Venues. Each Venue has a unique ID which is included in the response. I do this with the following in my ViewController:
[objectManager loadObjectsAtResourcePath:[NSString stringWithFormat:#"%#?%#", [URL resourcePath], [URL query]] delegate:self];
I then do the RestKit mapping etc and store the Venues in a data array. Everything working fine up to here.
At this point I loop through the Venues in the data array, and have to make a followup API call to retrieve more details about each Venue. For each Venue, I use the unique ID and call the Venue detail API (https://developer.foursquare.com/docs/venues/venues). This is the part that stumps me. I am trying to get an NSArray of Photo objects returned from this second API call. So far I have tried variations of this:
for (id venue in self.data){
Venue *myvenue = (Venue *)venue;
RKURL *URL = [RKURL URLWithBaseURL:[objectManager baseURL] resourcePath:[NSString stringWithFormat:#"/venues/%#", myvenue.venueid] queryParameters:queryParams];
[objectManager loadObjectsAtResourcePath:[NSString stringWithFormat:#"%#?%#", [URL resourcePath], [URL query]] delegate:self];
}
and in my mapping:
RKObjectMapping *photosMapping = [RKObjectMapping mappingForClass:[Photo class]];
[photosMapping mapKeyPathsToAttributes:#"url", #"imageURL", nil];
[venueMapping mapRelationship:#"photos" withMapping:photosMapping];
[objectManager.mappingProvider setMapping:photosMapping forKeyPath:#"response.photos"]; // not sure if this keypath is for the second API call
and my Venue class has this:
#property (strong, nonatomic) NSArray *photos;
Venue.photos always returns empty. Any suggestions?

From the code you have posted, nowhere are you linking the response with a venue. I'm not sure on the exact JSON returned by foursquare but you will need to conceive a way to map using your venue mapping which has a nested photo object. If foursquare does not return some reference to the original Venue object in it's response, you can try setting the RKObjectLoader's targetObject property to the Venue (myVenue in your code)

Not sure if this is the 'best' way to do it, but I got it working in the end. Paul de Lange's comment about not mapping the venue and the photos correctly set me off on the correct path.
The foursquare response was not in the format that I required in my app, so I had to use RKObjectLoader's willMapData to modify the response slightly before the rest of the mapping operation.
Here it is in case it helps anybody in the future:
- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout id *)mappableData {
NSArray* origPhotosArray = [*mappableData valueForKeyPath:#"response.venue.photos.groups"];
NSMutableArray* newPhotosArray = [[NSMutableArray alloc] init];
// <snip> Some extra code goes here where I discarded photos I did not want.... </snip>
// Create copies of objects in the format that I want
NSMutableDictionary *myphoto = [[NSMutableDictionary alloc] initWithDictionary:Photo];
NSString *venueID = [*mappableData valueForKeyPath:#"response.venue.id"];
[myphoto setObject:venueID forKey:#"assignedVenueid"];
[newPhotosArray addObject:myphoto];
// Replace the new objects instead of the response objects
[*mappableData removeObjectForKey:#"response.venue.photos.groups"];
[*mappableData setObject:newPhotosArray forKey:#"response.venue.photos.groups"];
}
And in my mapping:
RKManagedObjectMapping *photosMapping = [RKManagedObjectMapping mappingForClass:[Photo class] inManagedObjectStore:objectStore];
[photosMapping mapKeyPath:#"url" toAttribute:#"imageURL"];
[photosMapping mapKeyPath:#"assignedVenueid" toAttribute:#"assignedVenueid"];
[venueMapping mapRelationship:#"photos" withMapping:photosMapping];
[objectManager.mappingProvider setMapping:photosMapping forKeyPath:#"response.venue.photos.groups"];
[photosMapping hasOne:#"assignedVenue" withMapping:venueMapping];
[photosMapping connectRelationship:#"assignedVenue" withObjectForPrimaryKeyAttribute:#"assignedVenueid"];

Related

RestKit 0.10.0 with foursquare API's not retrieving the response when using blocks

Um beginner with RestKit, first example for me was on foursquare API's and I've used RestKit with Blocks not delegates.
I want to retrive the name's of venues,this is the JSON response
and this is my code:
// App Delegate.m
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURLString:#"https://api.Foursquare.com/v2"];
RKManagedObjectStore *objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:#"Venue.sqlite"];
objectManager.objectStore = objectStore;
objectManager.serializationMIMEType = RKMIMETypeJSON;
RKManagedObjectMapping *venueMapping = [RKManagedObjectMapping mappingForClass:[Venue class] inManagedObjectStore:objectStore];
[venueMapping mapKeyPath:#"id" toAttribute:#"id"];
[venueMapping mapKeyPath:#"name" toAttribute:#"name"];
venueMapping.primaryKeyAttribute = #"id";
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venue"];
then in myViewController.m
-(void)loadVenues{
// When caling loadObjectsAtResourcePath method it specify RKObjectLoader which is the actual request.
// within these block you can take more options to controll the request.
[[RKObjectManager sharedManager]loadObjectsAtResourcePath:#"/venues/40a55d80f964a52020f31ee3?oauth_token=FNQPN5P5EKLJ5IQ44TMWO00I3W033M0Y1TKINW2OTF2VIOTP&v=20130512" usingBlock:^(RKObjectLoader* loader)
{
loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[Venue class]];
loader.onDidLoadObject = ^(NSArray *objects)
{
NSLog(#"onDidLoadObject Blocks");
self.data = objects;
[self.tableView reloadData];
};
}
];
}
and the app is entering the block of onDidLoadObject but every time the array is empty !!
even when I test the link on browser it comes with data.
When I debug the loader.URL it always come with these
https://api.Foursquare.com/v2/venues/40a55d80f964a52020f31ee3?v=20130512&oauth_token=FNQPN5P5EKLJ5IQ44TMWO00I3W033M0Y1TKINW2OTF2VIOTP -- https://api.Foursquare.com/v2 -- https://api.Foursquare.com/v2
I don't know why load.URL is wrong ?!
I think um calling the 4square API's with the wrong way, anyone can help ? :)
-Put Your mapping as a class method to be accessed from all application classes, and done only once.
-change the Attribute "id" as it is reserved in Objective-C.
-add this to the block
[loader setObjectMapping:[RestKitMapping yourMapping]];
-then add this with your mapping code
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venue"];
-And use Delegates instead of blocks
I've two concerns regarding the above code:
1- Apparently the above JSON response, lists just one Venue .. So KeyPath should be "response.venue" not "response.venues"
2- Where's the mapping for ID? .. which is the primary key that RestKit uses to insert into DB? You need to set the primary key mapping.

updating RestKit: loadObjectsAtResourcePath: usingBlock: don't save objects in cache

I'm in a project where we use RestKit. We was using the 0.9.2 version until we decided to upgrade to 0.10.0. After some little changes in my code to make it compatible with the new version of RestKit I found a error when I use loadObjectsAtResourcePath: usingBlock:. When I receive the response in my delegate with the function -(void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects, the array of objects have the correct objects but they are not saved in the cache of coreData. If I try to access to access them with a fetch to coreData I don't find them. Here some code I use:
Configuration of RestKit:
RKObjectManager* objectManager = [RKObjectManager objectManagerWithBaseURL:[NSURL URLWithString:#"http://myweb.com/web/app.php/ws"]];
objectManager.client.requestQueue.showsNetworkActivityIndicatorWhenBusy = NO;
objectManager.objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:#"db.sqlite"];
RKManagedObjectMapping* boAccount = [RKManagedObjectMapping mappingForClass:[BOAccount class] inManagedObjectStore:objectManager.objectStore];
boAccount.primaryKeyAttribute = #"boAccountID";
[boAccount mapKeyPathsToAttributes: #"id", #"boAccountID",
#"created_at", #"createDate",
#"date_expiration", #"expirationDate",
#"date_billing", #"billingDate",
#"nb_authorized_devices", #"numberOfAuthorizedDevices",
nil];
[objectManager.mappingProvider addObjectMapping:boAccount];
[objectManager.mappingProvider setMapping:boAccount forKeyPath:#"account"];
The call to load loadObjectsAtResourcePath: usingBlock:
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/connection" usingBlock:^(RKObjectLoader *loader){
RKParams *params= [RKParams params];
//[params setValue:_password.text forParam:#"password"];
[params setValue:[Encrypt encryptWithPublicKeyMessage:_password.text] forParam:#"password"];
[params setValue:_email.text forParam:#"email"];
[params setValue:udid forParam:#"udid"];
[params setValue:name forParam:#"name"];
loader.delegate = self;
loader.params= params;
loader.method= RKRequestMethodPOST;
} ];
-(void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader{
BOAccount* account;
NSArray* accounts = [BOAccount allObjects];
account = [accounts objectAtIndex:0];
}
But the array accounts is empty. I tried -(void)objectLoaderDidFinishLoading:(RKObjectLoader *)objectLoader{ too but it gives me the same result. I know I can get the object I want with the array objects but I need to have the objects in the DB for my project. I know the objects are well parsed because I have see the array objects of objectLoaderDidFinishLoading and all is right.
Anyone know whats happening? Before the update this function works right!
Update:
I found that if I delete the application from the simulator or iPad sometimes it works the next time... I thought that it could help for fins an answer. Can be that I try to use the items before RestKit put it in cache?
You need to manually save the objects into the CoreData's database:
loader.onDidLoadObjects = ^(NSArray *objects) {
// Save:
NSError* error = nil;
[[RKObjectManager sharedManager].objectStore.managedObjectContextForCurrentThread save:&error];
if (error) {
NSLog(#"Save error: %#", error);
}
}

RestKit Warning - WARNING: Failed mapping nested object: (null)

I just started learning RestKit. I am trying to use it to get nearby venues using Foursquare apis. but every time I try " objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects " I get this warning "WARNING: Failed mapping nested object: (null)".
I am really confused. Following is my mapping code which is in init method:
_objectManager = [RKObjectManager objectManagerWithBaseURL:#"https://api.foursquare.com"];
RKObjectMapping *locationMapping = [RKObjectMapping mappingForClass:[Location class]];
[locationMapping mapKeyPath:#"address" toAttribute:#"address"];
[locationMapping mapKeyPath:#"city" toAttribute:#"city"];
[locationMapping mapKeyPath:#"state" toAttribute:#"state"];
[_objectManager.mappingProvider setMapping:locationMapping forKeyPath:#"location"];
RKObjectMapping *venueMapping = [RKObjectMapping mappingForClass:[Groups class]];
[venueMapping mapRelationship:#"location" withMapping:locationMapping];
[_objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"groups"];
RKObjectMapping *responseMapping = [RKObjectMapping mappingForClass:[Response class]];
[responseMapping mapRelationship:#"groups" withMapping:venueMapping];
[_objectManager.mappingProvider setMapping:responseMapping forKeyPath:#"response"];
return self;
Following is the code in "locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation "
NSString *resourceURLString = [NSString stringWithFormat:#"/v2/venues/search?ll=%f,%f&limit=10&client_id=LEA1MYN1Z1QWSBFIOA1T4FREVVNF0R1ADQKMA0AMGL3CN5N4&client_secret=M2W204MKEAO4KV5L4ZXUYWK2GC32PO0KT5PWYJQBP4FKBTMO",newLocation.coordinate.latitude, newLocation.coordinate.longitude];
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:resourceURLString delegate:self];
[_locationManager stopUpdatingLocation];
and finally this code is in "objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects"
_objects = [objects retain];
NSLog(#"object that i receive is %#",objects);
Following is the response i get in console:
2012-02-26 13:15:39.872 restKitPractice[8609:690b] W restkit.object_mapping:RKObjectMappingOperation.m:372 WARNING: Failed mapping nested object: (null)
2012-02-26 13:15:39.873 restKitPractice[8609:207] object that i receive is (
"Response: 0x6b5cb20"
)
You can check the Venues json from Foursquare using this link:
https://api.foursquare.com/v2/venues/search?ll=40.562362,-111.938689&limit=10&client_id=LEA1MYN1Z1QWSBFIOA1T4FREVVNF0R1ADQKMA0AMGL3CN5N4&client_secret=M2W204MKEAO4KV5L4ZXUYWK2GC32PO0KT5PWYJQBP4FKBTMO
Any help or suggestions would be appreciated as I am really struggling with this issue.
Thanks
Vik
You should take a look at the object mapping docs.
Your mapping definitions do not match the json you're trying to parse.
I realize this is an old question, but I've seen the same warning. The cause is that you have JSON like this {"response":{}} that you want to map to an object. Since there are no keys in the object, it essentially maps to nil. So, the warning is pretty harmless. You will not get a Response object back because the server essentially returned something that RestKit can't map because there are no keys in the JSON that match any of your attribute mappings. RestKit is letting you know that with the warning and falling back to not creating an object.

Does TWRequest work for the twitter streaming api?

I am trying to make a basic iphone app that shows nearby tweets. I was using the TWRequest object to accomplish this with the twitter search api. Unfortunately, I would actually like to mark the tweets on a map using their GPS coordinates and the search api doesn't seem to return the actual location that a tweet was made with any better accuracy than the city name.
As such, I think I need to switch to the streaming api. I am wondering if it is possible to continue using the TWRequest object in this case or if I need to actually switch over to using NSURLConnection? Thanks in advance!
Avtar
Yes, you can use a TWRequest object. Create your TWRequest object using the appropriate URL and parameters from the Twitter API doco, and set the TWRequest.account property to the ACAccount object for the Twitter account.
You can then use the signedURLRequest method of TWRequest to get an NSURLRequest which can be used to create an asynchronous NSURLConnection using connectionWithRequest:delegate:.
Once this is done, the delegate's connection:didReceiveData: method will be called whenever data is received from Twitter. Note that each NSData object received may contain more than one JSON object. You will need to split these up (separated by "\r\n") before converting each one from JSON using NSJSONSerialization.
It took me a bit of time to get this up and running, So I thought I aught to post my code for others. In my case I was trying to get tweets close to a certain location, so you will see that I used a locations parameter and a location struct I had in scope. You can add whatever params you want to the params dictionary.
Also note that this is bare bones, and you will want to do things such as notify the user that an account was not found and allow the user to select the twitter account they would like to use if multiple accounts exist.
Happy Streaming!
//First, we need to obtain the account instance for the user's Twitter account
ACAccountStore *store = [[ACAccountStore alloc] init];
ACAccountType *twitterAccountType = [store accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
// Request permission from the user to access the available Twitter accounts
[store requestAccessToAccountsWithType:twitterAccountType
withCompletionHandler:^(BOOL granted, NSError *error) {
if (!granted) {
// The user rejected your request
NSLog(#"User rejected access to the account.");
}
else {
// Grab the available accounts
NSArray *twitterAccounts = [store accountsWithAccountType:twitterAccountType];
if ([twitterAccounts count] > 0) {
// Use the first account for simplicity
ACAccount *account = [twitterAccounts objectAtIndex:0];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
[params setObject:#"1" forKey:#"include_entities"];
[params setObject:location forKey:#"locations"];
[params setObject:#"true" forKey:#"stall_warnings"];
//set any other criteria to track
//params setObject:#"words, to, track" forKey#"track"];
// The endpoint that we wish to call
NSURL *url = [NSURL URLWithString:#"https://stream.twitter.com/1.1/statuses/filter.json"];
// Build the request with our parameter
TWRequest *request = [[TWRequest alloc] initWithURL:url
parameters:params
requestMethod:TWRequestMethodPOST];
// Attach the account object to this request
[request setAccount:account];
NSURLRequest *signedReq = request.signedURLRequest;
// make the connection, ensuring that it is made on the main runloop
self.twitterConnection = [[NSURLConnection alloc] initWithRequest:signedReq delegate:self startImmediately: NO];
[self.twitterConnection scheduleInRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
[self.twitterConnection start];
} // if ([twitterAccounts count] > 0)
} // if (granted)
}];

How do I have two post routes for the same class in RestKit

Since I couldn't figure out how to set up two different POST resource paths for the same class , I tried manually creating the RKObjectLoader request but it seems to keep sending a GET request instead of a POST even though I've set the method to POST. Here is my code
User *user = [[User alloc] init];
user.uname = uname;
user.pwd = pwd;
RKObjectManager *svc = [RKObjectManager sharedManager];
RKObjectMapping* mapping = [svc.mappingProvider objectMappingForClass:[User class]];
// what I was using before I needed two post resource paths//[svc postObject:user mapResponseWith:mapping delegate:self];
RKObjectLoader *loader = [svc loadObjectsAtResourcePath:authResourcePath objectMapping:mapping delegate:self];
[loader setMethod:RKRequestMethodPOST];
loader.userData = [NSNumber numberWithInt:RequestLogin];
loader.params = [NSDictionary dictionaryWithObjectsAndKeys:
uname, #"uname",
pwd, #"pwd",
nil];
[loader setSourceObject:user];
[loader send];
[user release];
In cases where you have more than one path to POST or PUT to, the easiest thing to do is use the block form of the postObject: invocation and specify the destination resourcePath yourself:
[[RKObjectManager sharedManager] postObject:foo delegate:bar block:^(RKObjectLoader *loader) {
loader.resourcePath = #"/my/destinationPath";
}];
We may introduce a named route concept at some point that would let you disambiguate the routes using names, but for now its purely based on the HTTP verb.
Note that you do NOT and cannot register the secondary path on the router -- you are sidestepping it completely for the secondary path.
In order to complete Blake Watters answer if the different route need different objectMapping you will need to do:
[[RKObjectManager sharedManager] postObject:query delegate:saveJobQueryHandler block:^(RKObjectLoader* loader) {
loader.objectMapping = NEW_MAPPING;
loader.resourcePath = #"/other/url";
loader.targetObject = nil; // Important
}];
For more information about loader.targetObject = nil; read sendObject:delegate:block:

Resources