RestKit dynamic mapping with nested items - ios

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))

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"

Related

Modeling sub-collections in MongoDB Realm Sync

I'm new to MongoDB as well as to MongoDB Realm Sync. I was following the Realm Sync tutorial and Realm data model docs, but I wanted to learn more so I tweaked the Atlas collection structure as follows.
Projects > Tasks // i.e. tasks is a sub-collection in each project.
What I don't know is how to come up with Realm Sync Schema which can support Atlas sub-collections.
The best I came up with is a Schema where Tasks are modelled as an array within the Project. But, I'm worried that this can hit the 16MB (although a lot!) document limit for projects with a lot of the tasks.
{
"bsonType": "object",
"properties": {
"_id": {
"bsonType": "objectId"
},
"_partition": {
"bsonType": "string"
},
"name": {
"bsonType": "string"
},
"tasks": {
"bsonType": "array",
"items": {
"bsonType": "object",
"title": "Task",
"properties": {
"name": {
"bsonType": "string"
},
"status": {
"bsonType": "string"
}
}
}
}
},
"required": [
"_id",
"_partition",
"name",
],
"title": "Project"
}
Looking forward on how to model sub-collection the right way.
Edit
Here's my client side Realm models.
import Foundation
import RealmSwift
class Project: Object {
#objc dynamic var _id: String = ObjectId.generate().stringValue
#objc dynamic var _partition: String = "" // user.id
#objc dynamic var name: String = ""
var tasks = RealmSwift.List<Task>()
override static func primaryKey() -> String? {
return "_id"
}
}
class Task: EmbeddedObject {
#objc dynamic var name: String = ""
#objc dynamic var status: String = "Pending"
}
As far the CRUD operations are concerned, I only create a new project and read existing projects as follows.
// Read projects
realm.objects(Project.self).forEach { (project) in
// Access fields
}
// Create a new project
try! realm.write {
realm.add(project)
}
Your code looks great and your heading the right direction, so this answer is more explanation and suggestions on modeling than hard code.
First, Realm objects are lazily loaded which means they are only loaded when used. Tens of thousands of objects will have very little impact on a devices memory. So suppose you have 10,000 users and you 'load them all in'
let myTenThousandUsers = realm.objects(UserClass.self)
meh, no big deal. However, doing this
let someFilteredUsers = myTenThousandUsers.filter { $0.blah == "blah" }
will (could) create a problem - if that returns 10,000 users they are all loaded into memory possibly overwhelming the device. That's a Swift function and 'converting' Realms lazy data using Swift should generally be avoided (use case dependent)
The observation of this code using Swift .forEach
realm.objects(Project.self).forEach { (project) in
// Access fields
}
could cause issues depending on what's being done with those project objects - using them as a tableView dataSource could be trouble if there are a lot of them.
Second thing is the question about the 16Mb limit per document. For clarity an Atlas document is this
{
field1: value1,
field2: value2,
field3: value3,
...
fieldN: valueN
}
where value can be any of the BSON data types such as other documents, arrays, and arrays of documents.
In your structure, the var tasks = RealmSwift.List<Task>() where Task is an embedded object. While conceptually embedded objects are objects, I believe they count toward a single document limit because they are embedded (correct me if I am wrong); as the number of them grows, the size of the enclosing document grows - keeping in mind that 16Mb of text is an ENORMOUS of text so that would/could equate to millions of tasks per project.
The simple solution is to not embed them and have them stand on their own.
class Task: Object {
#objc dynamic var _id: String = ObjectId.generate().stringValue
#objc dynamic var _partition: String = ""
#objc dynamic var name: String = ""
#objc dynamic var status: String = "Pending"
override static func primaryKey() -> String? {
return "_id"
}
}
Then each one can be 16Mb, and an 'unlimited number' can be associated with a single project. One advantage of embedded objects is a type of cascade delete where when the parent object is deleted, the child objects are as well, but with a 1-many relationship from Project to Tasks - deleting a bunch of tasks belonging to a parent is easy.
Oh - another case for not using embedded objects - especially for this use case - is they cannot have indexed properties. Indexing can greatly speed up some queries.

Json Decode to swift class with dynamic value class/struct type

I have multiple responses which has similar pattern but one key value has always different object in response of json which i want to decode in base model where one key has variety of object type.
Response be like,
{
"status": true,
"message": "Success",
"data":[]
}
Here in data response it has any kind of array of objects or any single object
struct BaseResponseModel: Codable {
var status: Bool
var message: String
var data: DataClass
enum CodingKeys: String, CodingKey {
case message
case data
case status
}
}
what we can do here to make it single class with data type object pass,
Anyone please..!
Use Swift generics, and provide the type only at the time of decoding:
struct BaseResponseModel<DataType: Codable>: Codable {
var status: Bool
var message: String
var data: DataType
}
Usage:
let myData = try JSONDecoder().decode(BaseResponseModel<MyStruct>.self, from: data).data // For object
let myData = try JSONDecoder().decode(BaseResponseModel<[MyStruct]>.self, from: data).data // For array
Note: You don't need CodingKeys if the rawValues are the same.

How to replace/update dictionary in Array of Dictionary based on Type

I'm getting below JSON response from server, and displaying phone number on screen.
Now user can change/update any of phone number, so we have to update particular mobile number in same object and send it to server.
"phone_numbers": [
{
"type": "MOBILE",
"number": "8091212121"
},
{
"type": "HOME",
"number": "4161212943"
},
{
"type": "BUSINESS",
"number": "8091212344"
}
]
My model class is looks like this:
public struct Contact: Decodable {
public let phone_numbers: [Phone]?
}
public struct Phone: Decodable {
public let type: PhoneType?
public let number: String?
}
I'm struggling to update this JSON object for particular phone number.
For example, if I want to update BUSINESS number only in above array, What's best way to do it.
I'm using XCode 11 and Swift 5.
Because all your properties are defined as constants (let), nothing can be updated. You have to initialize and return a new Contact object with the updated phone numbers.
If you change the properties to var, then you can update:
public enum PhoneType: String, Decodable {
case mobile = "MOBILE"
case home = "HOME"
case business = "BUSINESS"
}
public struct Contact: Decodable {
public var phone_numbers: [Phone]?
mutating func update(phoneNumber: String, for type: PhoneType) {
guard let phone_numbers = self.phone_numbers else { return }
for (i, number) in phone_numbers.enumerated() {
if number.type == type {
self.phone_numbers![i].number = phoneNumber
}
}
}
}
public struct Phone: Decodable {
public var type: PhoneType?
public var number: String?
}
var contact = try! JSONDecoder().decode(Contact.self, from: jsonData)
contact.update(phoneNumber: "123456", for: .business)
I'm struggling to update this JSON object for particular phone number.
It shouldn't be a JSON object when you update it. Think of JSON as just a format for transferring data. Once transferred, you should parse it into something that you can work with, like an array of dictionaries or whatever. If you've done that, then more specific questions you might ask are:
How can I find a specific entry in an array?
How can I modify the fields of a struct?
How can I replace one entry in an array with another?
After looking at the definitions of your structures, I think the problem you're having probably has to do with how you've declared them:
public struct Phone: Decodable {
public let type: PhoneType?
public let number: String?
}
Because you used let to declare type and number, those fields cannot be changed after initialization. If you want the fields of a Phone struct to be modifiable, you need to declare them with var instead of let.
The same thing is true for your Contact struct:
public struct Contact: Decodable {
public let phone_numbers: [Phone]?
}
You've declared phone_numbers as an immutable array because you used let instead of var. If you want to be able to add, remove, or modify the array in phone_numbers, you need to use var instead.
The struct declarations you have right now work fine for reading the data from JSON because all the components of the JSON data are constructed using the values from the JSON. But again, you'll need to make those structs modifiable by switching to var declarations if you want to be able to make changes.
There are a couple of ways to approach this (I'm assuming PhoneType is an enum you have somewhere)
You can iterate over the array and guard for only business numbers, like so
for phone in phone_numbers{
guard phone.type == .MOBILE else { continue }
// Code that modifies phone
}
You can filter and iterate over the array, like so
phone_numbers.filter {$0.type == .BUSINESS }.forEach { phone in
// Modify phone here
}
You can then modify the right value in the array with it's index, like this
for (phoneIndex, phone) in phone_numbers.enumerated() {
guard phone.type == .BUSINESS else { continue }
phone_numbers[phoneIndex].type = ANOTHER_TYPE
}
Some can argue that the second is preferred over the first, because it is an higher order function, but in my day to day activities, I tend to use both and believe that this is a matter of taste

Archiving Custom Object in Swift - Extended

I am working on an iOS App on Swift 4.0. The app uses an 3rd party SDK where there is a model lets say,
class Customer: NSCopying, NSObject {
var name: String!
var age: Int!
var address: Address!
}
At that point I have no control to modify any properties and signature for the model as its inside SDK. But I need to store the object in disk/user defaults and load when needed.
Is it possible? If it is then how can I do that?
One way is to use SwiftyJSON to convert the model object to JSON data:
extension Customer {
func toJSON() -> JSON {
return [
"name": name
"age": age
"address": address.toJSON() // add a toJSON method the same way in an Address extension
]
}
static func fromJSON(_ json: JSON) -> Customer {
let customer = Customer()
customer.name = json["name"].string
customer.age = json["age"].int
customer.address = Address.fromJSON(json["address"]) // add a fromJSON method the same way
}
}
Now you can do something like saving to UserDefaults
UserDefaults.standard.set(try! Customer().toJSON().rawData(), forKey: "my key")
let customer = Customer.fromJSON(JSON(data: UserDefaults.standard.data(forKey: "my key")!))

Nested Entities in JsonResultsAdapter

I am trying to connect to a third party service using Breeze with a custom JsonResultsAdapter.
The third party service has the "metadata" related to an entity in the root node of the array, then the variables are in a "data" property on the "metadata" object.
The format has two ways of defining relationships. One is via a "#ref" field which references the id of another entity. The other is by having the related object defined inline (instead of the "#ref") which does not have an explicit id, but which is only ever referenced by the "parent" object.
The data looks like:
[{
"id" : "abc",
"type" : "foo",
"data": { "relationshipRef" : { "#ref" : "someid" } }
},
{
"id": "someid",
"type" : "bar",
"data" : { "relationshipInline" : { "type" : "baz",
"data" : { "something" : "whatever",
"innerRelation" : { "#ref" : "abc"}
}
}
}]
I'm currently (in JsonResultsAdapter's visitNode function) moving the properties in the "data" object up into the "root" node, and then replacing any object with an "#ref" property with the value of the "#ref" key and appending an ID to the end (so that relationships can use the original name in the EntityType). IE, the first object would become:
{
"id" : "abc",
"type" : "foo",
"relationshipRefID" : "someid"
}
This works for top level entities and relationships, but I'm having problems with the nested ones.
How would you approach solving this problem?
I was going to use ComplexTypes but the documentation mentioned that they cannot have "navigationProperties" (relationships), which as you can see above is required (the "innerRelation" property).
In some cases, the entities can be nested down to 3 levels or so.
Here is my current visitNode function:
visitNode: function(node, parseContext, nodeContext) {
if(node instanceof Object && node.type != null) {
if(node.deleted) {
//TODO: make sure the object is removed from the manager
return {ignore:true};
}
//We need to tweak the data structure to fit what breeze expects.
//It expects properties to be in the same level as the "metadata" for an object ("type" etc),
//So we need to move the properties from the data object into the node, and fix up relationships.
if(parseContext.entityManager.metadataStore.getEntityType(node.type, true) != null) {
var data = node.data;
for(var key in data) {
var prop = data[key];
//Move any foreign key fields to be "relationID":id instead of "relation":{"#ref":id}
if(prop instanceof Object) {
var ref = prop["#ref"];
if(ref != null) {
node[key+"ID"] = ref
data[key] = null;
continue;
}
}
//TODO: Handle inline references <- This is where I need help!
node[key] = data[key];
}
return {
entityType: node.type,
nodeId: node.id
}
}
else {
return {ignore:true};
}
}
}
Well, apparently I should have tested more before asking here.
It turns out that this works automatically based on the navigationProperties defined in the model! Awesome. I did have to generate ids for the inner nodes that did not have them, but that was simple.

Resources