iOS, Remote server search with RestKit - objective-c

I'm working on an app where I want to make a remote search to a server. I want RestKit to save the retrieved data to the database. I first perform a local search (which currently works) then I want to make the remote search and then update a table view with the new results.
I'm having two problems, 1. how should my mapping look like and 2. the json returns an array with two different kinds of objects.
The URL looks like this:
search.json?search=[search string]
The JSON it returns looks like this:
[
{
"event": {
"id": 2,
[...]
},
{
"news": {
"id": 16,
[...]
}
Where event and news is two kind of objects.
In my app I have three models, Post (abstract entity and superclass) NewsPost (subclass to Post) and Event (subclass to Post).
My mappings looks like this:
RKManagedObjectMapping* newsMapping = [RKManagedObjectMapping mappingForClass:[NewsPost class] inManagedObjectStore:objectManager.objectStore];
newsMapping.primaryKeyAttribute = #"newsId";
newsMapping.rootKeyPath = #"news";
[newsMapping mapKeyPath:#"id" toAttribute:#"newsId"];
RKManagedObjectMapping *eventMapping = [RKManagedObjectMapping mappingForClass:[CalendarEvent class] inManagedObjectStore:objectManager.objectStore];
eventMapping.primaryKeyAttribute = #"calendarId";
eventMapping.rootKeyPath = #"calendars";
[eventMapping mapKeyPath:#"id" toAttribute:#"calendarId"];
// These two works.
[objectManager.mappingProvider setObjectMapping:newsMapping forResourcePathPattern:#"/package_components/1/news"];
[objectManager.mappingProvider setObjectMapping:eventMapping forResourcePathPattern:#"/package_components/1/calendars"];
// I don't know how these should look/work.
// Since the search word can change
[objectManager.mappingProvider setObjectMapping:eventMapping forResourcePathPattern:#"/package_components/1/search\\.json?search="];
[objectManager.mappingProvider setObjectMapping:newsMapping forResourcePathPattern:#"/package_components/1/search\\.json?search="];
My search code looks like this (local search works):
- (void)setUpSearch
{
if (self.searchField.text != nil) {
[self.posts removeAllObjects];
[self.events removeAllObjects];
[self.news removeAllObjects];
// Search predicates.
// Performs local search.
NSPredicate *contactNamePredicate = [NSPredicate predicateWithFormat:#"contactName contains[cd] %#", self.searchField.text];
NSPredicate *contactDepartmentPredicate = [NSPredicate predicateWithFormat:#"contactDepartment contains[cd] %#", self.searchField.text];
[...]
NSArray *predicatesArray = [NSArray arrayWithObjects:contactNamePredicate, contactDepartmentPredicate, contactEmailPredicate, contactPhonePredicate, linkPredicate, titlePredicate, nil];
NSPredicate *predicate = [NSCompoundPredicate orPredicateWithSubpredicates:predicatesArray];
self.posts = [[Post findAllWithPredicate:predicate] mutableCopy];
if (self.posts.count != 0) {
self.noResultsLabel.hidden = YES;
for (int i = 0; i < self.posts.count; i++) {
Post * post = [self.posts objectAtIndex:i];
if (post.calendarEvent == YES) {
[self.events addObject:post];
} else {
[self.news addObject:post];
}
}
}
// reload the table view
[self.tableView reloadData];
[self performRemoteSearch];
}
}
- (void)search
{
[self setUpSearch];
[self hideKeyboard];
[self performRemoteSearch];
}
- (void)performRemoteSearch
{
// Should load the objects from JSON
// Note that the searchPath can vary depending on search text.
NSString *searchPath = [NSString stringWithFormat:#"/package_components/1/search.json?search=%#", self.searchField.text];
RKObjectManager *objectManager = [RKObjectManager sharedManager];
[objectManager loadObjectsAtResourcePath:searchPath delegate:self];
}
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects
{
// This never gets called.
// Should update my arrays and then update the tableview, but it never gets called.
// Instead I get Error Domain=org.restkit.RestKit.ErrorDomain Code=1001 "Could not find an object mapping for keyPath: ''
}
Any tips on how i should or could do would be greatly appreciated.

I haven't used Managed Objects before but the first thing to do here is to activate the restkit log over object mapping and network request so you can check what is restkit getting from the server and how the mapping is working.
//This can be added in your app delegate
RKLogConfigureByName("RestKit/Network", RKLogLevelDebug);
RKLogConfigureByName("RestKit/ObjectMapping", RKLogLevelTrace);
In second place, according to your JSON and that your search path changes, I think is better to use mapping for key path instead of resource path pattern. So you should try to map by key, like in this example:
RKObjectMapping* articleMapping = [RKObjectMapping mappingForClass:[Article class]];
[articleMapping mapKeyPath:#"title" toAttribute:#"title"];
[articleMapping mapKeyPath:#"body" toAttribute:#"body"];
[articleMapping mapKeyPath:#"author" toAttribute:#"author"];
[articleMapping mapKeyPath:#"publication_date" toAttribute:#"publicationDate"];
[[RKObjectManager sharedManager].mappingProvider setMapping:articleMapping forKeyPath:#"articles"];
And then load your data like:
- (void)loadArticles {
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/articles" delegate:self];
}
The other way to do this is to map by object, so RestKit detects the kind of object and performs the mapping and you make the request to any path.
If you have any question please leave a comment and I can improve my answer as needed.

I've never tried answering a question with a bounty before, let me try to give a useful answer from some recent work =)
1. how should my mapping look like
From your code, everything looks pretty fine. Are there any nesting of objects? Do you need to serialize for posting back to the server?
2. the json returns an array with two different kinds of objects.
Are your attributes the same (i.e. Event has a title, event has a date) with no surprises? If not, you have to use dynamic nesting.
If a resource path (i.e. your search path) receives a collection with different objects (your case), you have to use dynamic object mapping to load the objects.
Since you can edit the JSON structure, things can be simpler by leveraging on RestKit.
- Make sure the JSON has a root_key_path for the two different type of objects.
From an old experiment and some googling, RestKit can properly map a json output with different objects if they have proper rootKeyPaths. Resulting JSON should have a rough structure like:
{
"news" : [
{
"id" : 1,
"title" : "Mohawk guy quits"
},
{
"id" : 2,
"title" : "Obama gets mohawk"
}
],
"events" : [
{
"id" : 1,
"name" : "testing"
},
{
"id" : 2,
"name" : "testing again"
}
]
}
I cannot be sure 100% the above is correct. You can experiment by making your API return news only, if it works, then adding the events data into the mix.
- Load the objects from server
// Make a NS dictionary and use stringByAppendingQueryParameters
NSDictionary *searchParams = [NSDictionary dictionaryWithKeysAndObjects:
#"query",#"myQuery",
#"location",#"1.394168,103.895473",
nil];
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:[#"/path/to/resource.json" stringByAppendingQueryParameters:searchParams] delegate:objectLoaderDelegate];
- Handle the "real" searching in your objectLoader Delegate
If it worked, the objects should be mapped to your Coredata entities. You can perform a local search using the NSPredicate method you posted above.
I prefer the design pattern where RestKit uses loadObjects... to get data from the server and maps it, the rest of the processing is done locally. This decoupling makes things more "app-like". You can do other form of manipulation using NSPredicates.
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
// Do some processing here on the array of returned objects or cede control to a method that
// you've built for the search, like the above method.
}
One example, if the search use case is restaurants nearby, it will probably make sense to load all the restaurants within the current lat/lon, and then perform the local filtering by name using Coredata. Your server will heart you.
Let me know and I'll try to improve the answer further.

Related

iOS7 is there a lightweight JSON to Object mapper, (NOT RestKit or NSJSONSerialization)?

My current project uses AFNetworking 2.2 and in general refuses to compile when I add Restkit. Is there a way for me to get some equivalent of RKObjectMapping as defined below from some other lightweight library? I'm talking about taking JSON and turning it into a custom value object, not just a dictionary or array.
Google GSON for Android comes to mind, is there something like that for iOS?
What' I'm trying to accomplish:
static RKObjectMapping* mapping = nil;
+(RKObjectMapping*)objectMapping
{
if(mapping != nil)
{
return mapping;
}
//allows automatic unpacking of JSON payloads into Value Objects
//https://github.com/RestKit/RestKit/wiki/Object-Mapping
//JSON - VO
mapping = [RKObjectMapping mappingForClass:[MyVO class]];
[mapping addAttributeMappingsFromDictionary:#{
#"label": #"label",
#"icon": #"iconName",
#"action": #"actionName",
#"children": #"children"
}];
return mapping;
}
Have you tried NSJsonSerialization (https://developer.apple.com/library/ios/documentation/foundation/reference/nsjsonserialization_class/Reference/Reference.html)? It seems to accomplish what you need. I use it whenever I need to parse anything in JSON format,
-(id) initWithDictionary:(NSDictionary*) dict {
id self = [super init];
if (id) {
self.label = dict[#"label"];
self.icon = dict[#"iconName"];
self.action = dict[#"actionName"];
self.children = dict[#"children"];
}
return id;
}

RestKit: how to map URL param to object attribute

I've got REST service method like this one
/GetOfficeDocument?officeId=259
which returns an array of documents. Document in the app is a NSManagedObject object that has relationship to an office. How can I map officeId param to office relationship of my Document?
I know I should override objectLoader:willMapData:, but I don't know what exactly should I do inside of this method. The documentation is useless.
UPD. The response of the server looks like this:
[{"AddedDate":"\/Date(1261484400000+0400)\/","Title":"Some text","Uri":"\/Document\/News\/851"}]
As you see, officeId is not contained in response, only in URL. I can extract it in objectLoader:willMapData: using
[[[loader URL] queryParameters] objectForKey:#"officeId"]
but where should I put it next? Mappable data parameter is a mutable array, what should I place there? No idea.
You could try to inject the OfficeId value in each document item returned in the response like so:
- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout __autoreleasing id *)mappableData
{
NSString *officeId = [[[loader URL] queryParameters] objectForKey:#"officeId"];
NSMutableArray *newMappableData = [[NSMutableArray alloc] initWithCapacity:[*mappableData count]];
for (NSDictionary *documentDict in *mappableData)
{
NSMutableDictionary = newDocumentDict = [documentDict mutableCopy];
[newDocumentDict setObject:officeId forKey:#"OfficeId"];
[newMappableData addObject:newDocumentDict];
}
*mappableData = newMappableData;
}
And use something similar to the following in your Document mapping:
[documentMapping mapAttributes:#"AddedDate", #"Title", #"Uri", #"OfficeId", nil];
[documentMapping mapKeyPath:#"" toRelationship:#"office" withMapping:officeMapping];
[documentMapping connectRelationship:#"office" withObjectForPrimaryKeyAttribute:#"OfficeId"];
I usually add the RKObjectMapping to the managedObject class
Add this to your Document.h
+ (RKObjectMapping *)objectMapping;
Add this method to your Document.m
+ (RKObjectMapping *)objectMapping {
RKManagedObjectMapping *mapping = [RKManagedObjectMapping mappingForClass:[self class] inManagedObjectStore:[[RKObjectManager sharedManager] objectStore]];
mapping.primaryKeyAttribute = #"word";
[mapping mapKeyPath:#"word" toAttribute:#"word"];
[mapping mapKeyPath:#"min_lesson" toAttribute:#"minLesson"];
}
Off course you should change the key paths to your Document object properties. each pair is the name of the key on the server responds and it's corresponded keyPath on your managedObject.
Then when you initialize the objectManager you can set the mapping for each managedObject you have.
RKManagedObjectStore *store = [RKManagedObjectStore objectStoreWithStoreFilename:databaseName usingSeedDatabaseName:seedDatabaseName managedObjectModel:nil delegate:self];
objectManager.objectStore = store;
//set the mapping object from your Document class
[objectManager.mappingProvider setMapping:[SRLetter objectMapping] forKeyPath:#"Document"];
YOu can find a great tutorial here - RestKit tutorial. In the middle of the article you will find data about mapping.

Fetching a one-to-many core data relationship returns correct objects the first time but empty set all other times

I have an object, Workout, that has a one-to-many relationship with an object, Exercise.
Diagram of models: http://i.imgur.com/q1Mfq.png
When I create a Workout object, I add three Exercise objects to it by looping over
[self addExercisesObject:exercise]
and then save my managed object context. Then, from my controller for displaying a workout, I can successfully fetch the workout and its exercises (with a fetch request), as shown by the output in my debugger:
Printing description of self->_savedWorkout:
<Workout: 0x6c5a990> (entity: Workout; id: 0x6e46e00 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Workout/p1> ; data: {
bodyweight = nil;
date = "2012-05-09 16:59:43 +0000";
exercises = (
"0x6e3c870 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p3>",
"0x6e3eaf0 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p2>",
"0x6e36820 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Exercise/p1>"
);
isCompleted = 0;
workoutId = 1;
workoutPlan = "0x6e6c980 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/WorkoutPlan/p1>";
})
So far so good. However, if I close my app in my simulator and start it up again and perform the same fetch request in same view, the workout looks like this:
Printing description of self->_savedWorkout:
<Workout: 0x6ea8ff0> (entity: Workout; id: 0x6e8f9e0 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/Workout/p1> ; data: {
bodyweight = nil;
date = "2012-05-09 16:59:43 +0000";
exercises = (
);
isCompleted = 0;
workoutId = 1;
workoutPlan = "0x6c8a130 <x-coredata://EA417EAA-101A-4F04-8276-3C4A6CDF094D/WorkoutPlan/p1>";
})
It appears that it fetches the same workout object, but now exercises is an empty set. Actually, exercises first looks like this after the fetch request:
exercises = "<relationship fault: 0x8a93100 'exercises'>";
but once I do:
for (Exercise *exercise in self.savedWorkout.exercises)
self.savedWorkout.exercises resolves to an empty set. I do not edit the workout in anyway in any part of my app.
My fetch request is made by this method in my Workout class:
- (Workout *)getLatestWorkout
{
self.model = [[self.managedObjectContext persistentStoreCoordinator] managedObjectModel];
NSFetchRequest *fetchRequest = [self.model fetchRequestTemplateForName:#"getLatestWorkout"];
NSError *error = nil;
NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
if ([results count] == 1) {
return [results objectAtIndex:0];
}
return nil;
}
I made the fetch request template with Xcode's GUI tool. It fetches all Workout objects where isCompleted == 0. You can see that it fetches the same object each time because the workout's x-coredata path is the same in both debugger outputs.
Update: I checked my SQLite database. There is one workout in the workout table and three exercises in the exercises table.
Any ideas what's going on?
EDIT: Code that creates objects posted below
- (void)storeUserSettings
{
// get the file path if it exists
NSString *path = [[NSBundle mainBundle] pathForResource:#"userSettings" ofType:#"plist"];
// create it if it doesn't
if (path == nil) {
path = [NSString stringWithFormat:#"%#%#",
[[NSBundle mainBundle] bundlePath], #"/userSettings.plist"];
}
// and write the new settings to file
[self.userSettings writeToFile:path atomically:YES];
// load managed object context
[self loadMOC];
WorkoutPlan *currentPlan = [[WorkoutPlan alloc] getActiveWorkoutPlan];
[currentPlan setManagedObjectContext:self.managedObjectContext];
// if user has no plan or is changing plans, create new plan and first workout
if (currentPlan == nil ||
([self.userSettings valueForKey:#"plan"] != currentPlan.planId)) {
// create a workoutPlan object
WorkoutPlan *workoutPlan = [[WorkoutPlan alloc] initWithEntity:
[NSEntityDescription entityForName:#"WorkoutPlan"
inManagedObjectContext:self.managedObjectContext]
insertIntoManagedObjectContext:self.managedObjectContext];
// set attributes to values from userSettings and save object
[workoutPlan createWorkoutPlanWithId:[self.userSettings valueForKey:#"plan"]
schedule:[self.userSettings valueForKey:#"schedule"]
dateStarted:[self.userSettings valueForKey:#"nextDate"]];
}
// if user is just changing schedule, update schedule of current plan
else if (![currentPlan.schedule isEqualToString:[self.userSettings valueForKey:#"schedule"]]) {
[currentPlan setSchedule:[self.userSettings valueForKey:#"schedule"]];
[currentPlan saveMOC];
}
}
- (void)loadMOC
{
AppDelegate *delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
self.managedObjectContext = delegate.managedObjectContext;
self.model = [[self.managedObjectContext persistentStoreCoordinator] managedObjectModel];
}
- (void)createWorkoutPlanWithId:(NSNumber *)planId schedule:(NSString *)schedule
dateStarted:(NSDate *)dateStarted
{
[self deactivateCurrentPlan];
// set workout plan attributes
[self setPlanId:planId];
[self setIsActive:[NSNumber numberWithBool:YES]];
[self setSchedule:schedule];
[self setDateStarted:dateStarted];
// create first workout and add to workout plan
Workout *firstWorkout = [NSEntityDescription
insertNewObjectForEntityForName:#"Workout"
inManagedObjectContext:self.managedObjectContext];
[firstWorkout setManagedObjectContext:self.managedObjectContext];
[firstWorkout createFirstWorkoutForPlan:self onDate:dateStarted];
[self addWorkoutsObject:firstWorkout];
[self saveMOC];
}
- (void)createFirstWorkoutForPlan:(WorkoutPlan *)plan onDate:(NSDate *)startDate
{
// set workout attributes
[self setDate:startDate];
[self setIsCompleted:[NSNumber numberWithBool:NO]];
[self setWorkoutId:[NSNumber numberWithInt:1]];
NSArray *exerciseList = [self getExercisesForWorkout:self inPlan:plan];
// iterate over exercises in spec and create them
for (NSDictionary *exerciseSpec in exerciseList)
{
// create a exercise MO
Exercise *exercise = [NSEntityDescription
insertNewObjectForEntityForName:#"Exercise"
inManagedObjectContext:[plan managedObjectContext]];
[exercise setManagedObjectContext:[plan managedObjectContext]];
[exercise createExerciseForWorkout:self withSpec:exerciseSpec];
// add exercise to workout object
[self addExercisesObject:exercise];
}
}
- (void)createExerciseForWorkout:(Workout *)workout withSpec:exerciseSpec
{
// set exercise attributes
self.exerciseId = [exerciseSpec valueForKey:#"id"];
self.isPersonalRecord = [NSNumber numberWithBool:NO];
NSArray *sets = [exerciseSpec valueForKey:#"sets"];
int i = 1;
for (NSNumber *setReps in sets)
{
// create a set MO
Set *set = [NSEntityDescription
insertNewObjectForEntityForName:#"Set"
inManagedObjectContext:[workout managedObjectContext]];
[set setManagedObjectContext:[workout managedObjectContext]];
// set set attributes
set.order = [NSNumber numberWithInt:i];
set.repetitions = setReps;
set.weight = [exerciseSpec valueForKey:#"default_weight"];
// add set to exercise object
[self addSetsObject:set];
i++;
}
}
I had a similar problem. The parent-child relationship worked when the app was running but after re-start only the latest child record was retrieved.
I was adding the children like this:
create the child record
set the child's parent attribute, set the child's other
attributes
add the child to the parent using the parent's add method
I found that it was fixed if I did it like this:
create the child record
add the child to the parent using the parent's add method
set the child's parent attribute, set the child's other
attributes
Core Data is complex. There could be dozens of things to check, any one thing which could be causing issues.
How many MOCs are you using? How are you saving? Many more questions...
I would suggest turning on the SQL debugging flag (-com.apple.CoreData.SQLDebug 1) in the EditScheme for arguments when starting the application.
Run your code, and see what is actually going on.
Relationships resolve to a fault when fetched, unless you override it in the fetch request.
If you are using more than one MOC in a parent/child relationship, the save from the child to the parent just puts data into the parent, it does not really save it. If using UIManagedDocument, it's a whole different set of issues...
I hope this does not sound harsh. Be prepared to provide a whole lot of information for a Core Data question, other than "this is not saving and here is some debugging output."
Basically, how CoreData works depends on how the stack is created, whether using UIManagedDocument or not, multiple threads, how creating objects, how saving them, options on fetch requests, and a whole lot more things.
It's actually not that complex, but there are lots of customizations and special cases depending on how it is used.
EDIT
Post the code that creates the objects/relationships.
Also, try the fetch with a manual fetch request instead of the template. When you look at the data in the database, do you see the foreign keys for the relationships set appropriately?
Run it all again with debugging enabled to see exactly what the SQL is doing. That is more valuable that your own debugging output.
I'm having this exact same problem but my model is pretty complex. My app creates the entities and relationships on startup if they don't already exist. If they are created and I don't exit the app, I'm able to fetch an entity with a to-many relationship and see the correct count of related objects. If I exit my app and restart it (it now knows it doesn't have to create a default set of data) then the relationships are returning a null set. I can't figure it out.
EDIT: I figured out that my problem relates to an Ordered set relation. I had to use a Category to create a work around (found on stack overflow) to insert new entries into an ordered set. So I'm guessing that has something to do with it.

Mapping a JSON response to an object using RestKit and Objective-C

I am relatively new to Objective-C and am attempting to use RestKit to receive a JSON response from a web service. I have successfully received the data back to my application, which looks like this viewing the response:
{id:"1","Translation":"Test"}
I would like to map this translation to my "Translation" object in my application, but have tried a few different ways but am not sure how to achieve this.
So my questions are:
How can I map this response to my Translation object
Am I doing this correctly, creating a method to complete this call outwit my view controller?
My Translation Object
#implementation Translation
#synthesize identifier = _identifier;
#synthesize translation = _translation;
- (NSDictionary*)elementToPropertyMappings {
return [NSDictionary dictionaryWithKeysAndObjects:
#"id", #"identifier",
#"translation", #"translation",
nil];
}
#end
My Translate Method
- (NSString *)performTranslation:(NSString *)translation
{
NSString *data = [[NSString alloc] initWithFormat:#"{\"SourceId\": \"%#\",\"RegionTag\": \"%#\",\"InputString\": \"%#\"}", #"1", #"Glasgow", translation];
NSString *post = data;
RKRequest *MyRequest = [[RKRequest alloc] initWithURL:[[NSURL alloc] initWithString:#"http://my.url.com/Translation/Translate"]];
MyRequest.method = RKRequestMethodPOST;
MyRequest.HTTPBodyString = post;
MyRequest.additionalHTTPHeaders = [[NSDictionary alloc] initWithObjectsAndKeys:#"application/json", #"Content-Type", #"application/json", #"Accept", nil];
[MyRequest send];
RKResponse *Response = [MyRequest sendSynchronously];
return Response.bodyAsString; <--- looking to map this to translation object here
}
The snippet of your code seems a bit outdated. I strongly recommend reading the newest Object Mapping guide in order to leverage RestKit into it's fullest potential - especially the part Mapping without KVC.
Edit:
In order to post an object with RestKit and receive back an answer, we define a TranslationRequest class that will hold our request & Translation to hold our response.
Firstly, we set up our RKObjectManager and mappings (i usually do this in my AppDelegate):
RKObjectManager *manager = [RKObjectManager objectManagerWithBaseURL:kOurBaseUrl];
[manager setSerializationMIMEType:RKMIMETypeJSON];
//this is a singleton, but we keep the manager variable to avoid using [RKObjectManager sharedManager] all the time
//Here we define a mapping for the request. Note: We define it as a mapping from JSON to entity and use inverseMapping selector later.
RKObjectMapping *translationRequestMapping = [RKObjectMapping mappingForClass:[TranslationRequest class]];
[translationRequestMapping mapKeyPath:#"RegionTag" toAttribute:#"regionTag"];
...
[[manager mappingProvider] setSerializationMapping:[translationRequestMapping inverseMapping] forClass:[TranslationRequest class]];
//now we define the mapping for our response object
RKObjectMapping *translationMapping = [RKObjectMapping mappingForClass:[Translation class]];
[translationMapping mapKeyPath:#"id" toAttribute:#"identifier"];
[translationMapping mapKeyPath:#"Translation" toAttribute:#"translation"];
[[manager mappingProvider] addObjectMapping:mapping];
//finally, we route our TranslationRequest class to a given endpoint
[[manager router] routeClass:[TranslationRequest class] toResourcePath:kMyPostEndpoint];
This should be enough of the necessary setup. We can call our backend anywhere in the code (e.g. in any controller) like this:
//we create new TranslationRequest
TranslationRequest *request = [[TranslationRequest alloc] init];
[request setRegionTag:#"Hello"];
....
//then we fetch the desired mapping to map our response with
RKObjectMapping *responseMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:class]
//and just call it. Be sure to let 'self' implement the required RKObjectManagerDelegate protocol
[[RKObjectManager sharedManager] postObject:request mapResponseWith:responseMapping delegate:self];]
Try this approach and let me know if you need any assistance.. I was not able to test it fully as i don't have any suitable backend that will return the responses, but judging from the RestKit log this should work.
You need to pass the returned JSON string into a JSON parser. I use SBJSON. You can then use the resulting dictionary to populate the properties of your object.
RestKit seems to have native objects that encapsulate four different JSON parsers. However, I'd advise caution because they seem to assume that the top level parsed object will always be a dictionary.
As another aside, the example in your question is not valid JSON. It should look like this:
{"id":"1","Translation":"Test"}

More than one RKObjectManager at a time (RestKit)

I am testing out RestKit and need to access different BaseUrls and also sometimes access a web service with the same baseUrl from different places "at once", lastly I also need to access the same baseUrl with different ressourcePaths in the same controller.
In my app delegate I set up the RKObjectManager singleton like this.
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:kBaseUrl];
[objectManager registerClass:[EntityClass1 class] forElementNamed:#"element1"];
[objectManager registerClass:[EntityClass2 class] forElementNamed:#"element2"];
.
.
.
etc.
The singleton approach is really easy to work with, I however can't figure out how to separate the different web service calls.
In MyViewController, which implement the RKObjectLoaderDelegate, I will have the two methods:
- (void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects {
//stuff with result
}
- (void)objectLoader:(RKObjectLoader *)objectLoader didFailWithError:(NSError *)error {
//stuff with error
}
This causes no problems when MyViewController uses one RKObjectManager singleton to access one ressourcePath with one baseUrl.
If I start different requests in this way:
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:FLICKRPath delegate:self]
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:FOURSQUAREPath delegate:self]
and so on, within the same MyController, my problem is that FLICKRPath and FOURSQUAREPath of course has different baseUrl, but the RKObjectManager only has one?
If I get this working and can have different RKObjectManagers another problem arises.
The delegate methods didLoadObjects and didFailWithError will receive results from both RKObjectManagers and I can't see any other way to tell them apart than from their baseUrls. Potentially comparing each return value with a baseUrl and, even worse, a ressourcePath, in the delegate method does not appeal to me at all.
If I have different RKObjectManagers I guess I could pass them different delegates and build classes dedicated to deal with the return values from different baseUrls and ressourcePaths. This would mean I had to build yet another abstraction on top of MyController and RestKit, which also seems messy.
I have a strong feeling I am going about this in the wrong way, the RestKit source is very impressive which indicates that is me fighting the framework. I would really appreciate some best practice insights on the subject. I have been through all the resources and examples that I could find but have not seen the above use case. It is always one RKObjectManager, one baseUrl and one ressourcePath.
Thank you in advance.
Since there is no accepted answer yet: using multiple object managers is quite simple using RestKit.
From the Wiki (Using Multiple Base URLs (and Multiple Object Managers):
The first object manager you create will be the shared singleton
RestKit uses by default. But by creating additional object managers,
you can pull from their BaseURLs as needed, just be sure to retain
these new managers.
RKObjectManager *flickrManager =
[RKObjectManager objectManagerWithBaseURL:flickrBaseUrl]; // <-- shared singleton
RKObjectManager *foursquareManager =
[[RKObjectManager objectManagerWithBaseURL:foursquareBaseUrl] retain]; // <-- you must retain every other instance.
Depending on your application, you may want to put this second object
manager in a more accessible place, like a retained property on the
AppDelegate, so that it's easy to pull from as needed.
In the event that you need to differentiate between the results from
your two (or more) object managers, simply set an identifier in the
userData for the queries.
- (void)someAction(id)sender {
// .......
RKObjectLoader* loader = [[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/whatever" delegate:self];
loader.userData = #"foursquare";
// or do this, if you need a number instead of a string
loader.userData = [NSNumber numberWithInt:1234];
// .......
}
//Then when the delegate comes back you can cast it into a string or number as appropriate:
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
// .......
NSString* source = (NSString*) objectLoader.userData;
// or, if you did the NSNumber instead:
NSNumber* source = (NSNumber*) objectLoader.userData;
// .......
}
API change:
RKObjectLoader* loader = [[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/whatever" delegate:self];
doesn't compile in RestKit v.0.10.3 (loadObjectsAtResourcePath:delegate: returns void). That method just wraps a few lines of code, though, so you can still get at the loader, and add userData, with the following:
RKObjectLoader *loader = [[RKObjectManager sharedManager] loaderWithResourcePath:resourcePath];
loader.userData = #"SOMEDATA";
loader.delegate = self;
loader.method = RKRequestMethodGET;
[loader send];
(adding note in case other new users run into the same issues I did).
And by the way, since userData property is also available on RKRequest, you can use the same approach for loading/identifying requests.
For example, some post request:
RKClient * client = [RKClient sharedClient];
[client post:#"/your-api-path" usingBlock:^(RKRequest *request) {
request.userData = #"<some-object-you-can-check-in-delegate-callback>";
request.params = someParamsForRequest;
request.delegate = <delegate you want to call when request is finished>;
}];
How about using objectLoader.
You'll find the mapped object type/Class objectLoader.objectMapping.objectClass and add your conditions based on it instead of the url
-(void)objectLoader:(RKObjectLoader *)objectLoader didLoadObjects:(NSArray *)objects {
// your condition based on -> objectLoader.objectMapping.objectClass
}
Hope it will help
Possible approach is to introduce one singletone for each base url.
You can instantiate as many RKObjectManager objects as you want. However, only the first one will become shared. Look into initWithHTTPClient: sources.
if (nil == sharedManager) {
[RKObjectManager setSharedManager:self];
}
We can't use default sharedManager method to target specific object manager but we can easily implement our own singleton. Here's an example for Google Maps object manager:
#implementation GMObjectManager
+ (GMObjectManager*)sharedManager
{
static GMObjectManager *manager; // keep reference
if (!manager) {
// init with custom base url
NSURL *baseUrl = [NSURL URLWithString:kGMBaseUrl];
manager = [GMObjectManager managerWithBaseURL:baseUrl];
}
return manager;
}
- (id)initWithHTTPClient:(AFHTTPClient *)client
{
self = [super initWithHTTPClient:client];
if (self) {
// additional initialization
}
return self;
}
#end
Usage:
CGObjectManager *googleMapsManager = [GMObjectManager sharedInstance];