Search code examples
iosdynamicnestedmappingrestkit

RestKit dynamic mapping with nested items


I am having trouble configuring a dynamic mapping for an array of results. The result objects I'd like to map are embedded within a "container" that describes the type of the enclosed object.

I have JSON similar to the following:

"list": {
    "name": ""
    "item_infos": [
    {
        "type": "TaskItem",
        "item": {
            "id": 0
            "title": "A Task Item",
            "task": "..."
        }
        "url": "..."
    },
    {
        "type": "ReminderItem",
        "item": {
            "id": 0
            "title": "A Reminder Item",
            "reminder": "..."
        }
        "url": "..."
    }]
}


I'd like to map this as a List object with an array of Items, where each item might be of a different type. The different types are finite and known.

class List: NSManagedObject {
    @NSManaged var name: String
    @NSManaged var items: NSOrderedSet
}

class Item: NSManagedObject {
    @NSManaged var identifier: Int32
    @NSManaged var title: String

    // Each mappable `Item` is responsible for providing its own
    // mapping object. It's overridden in the subclasses below.
    class func responseMappingForManagedObjectStore(dos: RKManagedObjectStore) -> RKEntityMapping { ... }
}

class TaskItem: Item {
    @NSManaged var task: String
}

class ReminderItem: Item {
    @NSManaged var reminder: String
}


How can I map the embedded item directly under the list using RKDynamicMapping while still making use of the type field? I'm trying something along these lines for the response mapping:

let listMapping = RKEntityMapping(forEntityForName: "List", inManagedObjectStore: store)
responseMapping.addAttributeMappingsFromDictionary(["name": "title"])

let dynamicItemMapping = RKDynamicMapping()
dynamicItemMapping.setObjectMappingForRepresentationBlock { representation in

    let itemMapping: RKEntityMapping

    switch representation.valueForKeyPath("type") as? String {
    case .Some("TaskItem"):     itemMapping = TaskItem.responseMappingForManagedObjectStore(objectStore)
    case .Some("ReminderItem"): itemMapping = ReminderItem.responseMappingForManagedObjectStore(objectStore)
    default:                    return nil

    // This is the bit I'm failing to solve. How can I return a mapping
    // that essentially "skips" a level of the JSON, and just maps the
    // embedded `item`, not the `item_info`.
    let itemInfoMapping = RKObjectMapping(forClass: NSMutableDictionary.self)
    itemInfoMapping.addRelationshipMappingWithSourceKeyPath("item", mapping: itemMapping)
    return itemInfoMapping
}

listMapping.addPropertyMapping(RKRelationshipMapping(
    fromKeyPath: "item_infos",
    toKeyPath:   "items",
    withMapping: dynamicItemMapping))

With this mapping, an exception is raised:

-[__NSDictionaryM _isKindOfEntity:]: unrecognized selector sent to instance 0x7fd2603cb400

Which doesn't surprise me, as the way the dynamic mapping is set up just doesn't feel right anyway – I'm looking for a way to "skip" a level of the JSON and only map the embedded item.


An alternative attempt was to fully specify the fromKeyPath as "item_infos.item" for the relationship to the listMapping, but then I cannot use the type field in the dynamic mapping block to determine the type of Item mapping to use:

// ...
dynamicItemMapping.setObjectMappingForRepresentationBlock { representation in
    // `type` inaccessible from the nested item representation
    switch representation.valueForKeyPath("type") as? String {
    case .Some("TaskItem"):     return TaskItem.responseMappingForManagedObjectStore(objectStore)
    case .Some("ReminderItem"): return ReminderItem.responseMappingForManagedObjectStore(objectStore)
    default:                    return nil
}

listMapping.addPropertyMapping(RKRelationshipMapping(
    fromKeyPath: "item_infos.item",
    toKeyPath:   "items",
    withMapping: dynamicItemMapping))

Solution

  • You can't do exactly what you're trying to do, because you're connecting a core data relationship to what will be an array of dictionaries which contains managed objects.

    The simplest solution is to take your skip logic out from where it is and create all of the mappings for the managed objects (task and reminder) by adding item. at the start of the source key (path). So

    "item.title" : "title"