Anders
Anders

Reputation: 2941

iOS, Remote server search with RestKit

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.

Upvotes: 5

Views: 1054

Answers (2)

Damon Aw
Damon Aw

Reputation: 4792

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.

Upvotes: 1

clopez
clopez

Reputation: 4372

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.

Upvotes: 3

Related Questions