Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with NSManagedObject class conforming to Mappable #68

Closed
hopiaw opened this issue Feb 17, 2015 · 42 comments
Closed

Issue with NSManagedObject class conforming to Mappable #68

hopiaw opened this issue Feb 17, 2015 · 42 comments

Comments

@hopiaw
Copy link

hopiaw commented Feb 17, 2015

Hi,

I'm trying to map directly JSON to a class model inheriting NSManagedObject. I created an extension that conform to Mappable and implement mapping.

At compile time, I got a Swift compiler error, usr/bin/swiftc failed to exit code 1.

It might be due to the init() method of the Mappable protocol. When reading the Mapper.swift class I found the following:

   /**
* Maps a JSON dictionary to an existing object that conforms to Mappable.
* Usefull for those pesky objects that have crappy designated initializers like NSManagedObject
*/
public func map(JSON: [String : AnyObject], var toObject object: N) -> N {
    let map = Map(mappingType: .fromJSON, JSONDictionary: JSON)
    object.mapping(map)
    return object
}

which would be the function to use in order to set properties of the entity once created.

Could you post an example of an NSManagedObject class conforming to Mappable?

Thanks

@tristanhimmelman
Copy link
Owner

Hi, I have not actually used ObjectMapper with an NSManagedObject as of yet. However perhaps @brandonroth can provide insight, as he is the one who contributed the above function.

@funky-monkey
Copy link

YES! I'm having the same problem as well.
Can there be done something about the required init in the Mappable protocol?

@tristanhimmelman
Copy link
Owner

The required init is necessary so that ObjectMapper can instantiate objects during the mapping process. I can't think of a way to remove it at the moment. I'll put some more thought into it.

@tristanhimmelman
Copy link
Owner

The latest release has an update to the Mappable protocol. It no longer requires the default initializer function. This may resolve the issue you all have been having.

@funky-monkey
Copy link

Great stuff!
What i've seen so far, is that if i omit the init method, the compiler will complain. I think it is a failable initializer, not an optional initializer. But please prove me wrong ;-)

@bonebox
Copy link

bonebox commented Apr 8, 2015

It is still required in 0.9. Am I missing something?

@tristanhimmelman
Copy link
Owner

init? is part of Mappable but init is not anymore.

public protocol Mappable {
    init?(_ map: Map)
    mutating func mapping(map: Map)
}

@bonebox
Copy link

bonebox commented Apr 8, 2015

Since it still has to be declared, I get the compiler error:

'Element.swift:20:12: Initializer requirement 'init' can only be satisfied by a `required` initializer in non-final class 'Element'

And when I add required, it forces me to call the designated initializer for NSManagedObject.

@vincent-ch
Copy link

Hi @bonebox , I'm facing the same problem. How do you resolved it ?

@bonebox
Copy link

bonebox commented May 13, 2015

Hi @vincent-ch , I actually didn't resolve it. I ended up going with a different library, called Sync. It is awesome if you're mapping into managed objects!

@vincent-ch
Copy link

Thanks for the hint. If anyone would be interested in this case, I don't know if this is the proper way to do it, but I resolved it this way:

class AbstractModel: NSManagedObject, Mappable {

    @NSManaged var uuid: String?
    @NSManaged var updatedAt: String?
    @NSManaged var createdAt: String?

    override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
         super.init(entity: entity, insertIntoManagedObjectContext: DBUtils().getManagedObjectContext())
     }

    required init?(_ map: Map) {
        var ctx = NSManagedObjectContext.MR_defaultContext()
        var entity = NSEntityDescription.entityForName("AbstractModel", inManagedObjectContext: ctx)
        super.init(entity: entity!, insertIntoManagedObjectContext: ctx)

        mapping(map)
     }

    func mapping(map: Map) {
        uuid        <- map["uuid"]
        updatedAt   <- map["updatedAt"]
        createdAt   <- map["createdAt"]

     }
}

@Wii2zO
Copy link

Wii2zO commented Jul 30, 2015

Hey,

I use the method of @vincent-ch to map my JSON and store the POJO to the database, I succeed with one class, but how do that with a member which is an Array of another object ?
ObjectMapper use a simple Array to do this and it works perfectly, but CoreData use "NSSet" for relationships with another table...
I don't know how to do...

@vincent-ch
Copy link

Hi @Wii2zO , I faced the same problem and solved it that way: I created a temp array (this will not be persisted) to let ObjectMapper do its magic and then initialized the NSSet with that array using NSSet(array: elementsArray!) .

Hope this helps! (:

Here's a full exemple:

@objc(Gallery)
class Gallery: AbstractModel, Mappable {

@NSManaged var title: String?
@NSManaged var shootingDate: String?
@NSManaged var elements: NSSet?

private var elementsArray: [Image]?

override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
    super.init(entity: entity, insertIntoManagedObjectContext: DBUtils().getManagedObjectContext())
}

required init?(_ map: Map) {
    var ctx = DBUtils().getManagedObjectContext()
    var entity = NSEntityDescription.entityForName("Gallery", inManagedObjectContext: ctx)
    super.init(entity: entity!, insertIntoManagedObjectContext: ctx)

    mapping(map)
}

override func mapping(map: Map) {

    super.mapping(map)

    title           <- map["title"]
    shootingDate    <- map["shootingDate"]
    elementsArray   <- map["elements"]

    elements = NSSet(array: elementsArray!)
    elementsArray = nil

}

}

@Wii2zO
Copy link

Wii2zO commented Jul 30, 2015

Thank you for your response !
It seems to work... !

@HRiffiod
Copy link

HRiffiod commented Aug 5, 2015

Hi there, I've been trying @vincent-ch 's code but I'm having some troubles mapping custom NSManaged classes. Here's an example of my class :

import Foundation
import CoreData

class Credential: NSManagedObject, Mappable {

@NSManaged var credentialArrivalDate: String?
@NSManaged var credentialBarCode: String?
@NSManaged var credentialComment: String?
@NSManaged var credentialCountCheckIn: Int
@NSManaged var credentialCountCheckOut: Int
@NSManaged var credentialDepartureDate: String?
@NSManaged var credentialId: String?
@NSManaged var credentialIsArrived: Bool
@NSManaged var credentialIsCheckedOut: Bool
@NSManaged var credentialIsHost: Bool
@NSManaged var credentialLastModificationDate: String?
@NSManaged var credentialMemberControlled: String?
@NSManaged var credentialNumberOfPeople: Int
@NSManaged var credentialRSVP: Int
@NSManaged var credentialTypeId: String?
@NSManaged var credentialTypeName: String?
@NSManaged var credentialZoneId: String?
@NSManaged var credentialZoneName: String?
@NSManaged var credentialHosts: Set<Host>?
@NSManaged var credentialMember: Member
@NSManaged var credentialExtensionFields: Set<ExtensionFields>?
@NSManaged var credentialDeltaFirstCheckIn: String?
@NSManaged var credentialDeltaFirstCheckOut: String?
@NSManaged var credentialIsCheckedIn: Bool

}

I have a relationship to a Member NSManaged class, and to-many relationships to Host and ExtensionFields.

My goal is to build JSON data from the credential class (including the relationships). I did have some success with a custom parser ( see my post on stack-overflow : http://stackoverflow.com/questions/31672558/ios-swift-serialize-a-nsmanagedobject-to-json-with-relationships/31697576?noredirect=1#comment51347010_31697576)

BUT to conform to the json data that the web service needs, I need to map my class because the variables don't have the same names etc.

That said, I'm getting error (found nil while unwrapping) with @vincent-ch' s code (the difference being I use Set and not NSSet, could it be the problem here ?)

Is there a way to map my JSON string just to change variables name or can ObjectMapper do the magic for me ?

Thanks for your help,

Hugo

@vincent-ch
Copy link

Hi @HRiffiod ,

I don't know if the difference between Set and NSSet could be the problem, but I always try to use only the types generated by XCode (from the .xcdatamodel file). Could you try with NSSet?

And could you gave us the code line where you get the error?

@HRiffiod
Copy link

HRiffiod commented Aug 5, 2015

Hi @vincent-ch ,

so I added this code to my class :

// temporary array tos allow mapping
var credentialHostsArray: [Host]?
var credentialExtensionFieldsArray: [ExtensionFields]?

class func newInstance() -> Mappable {
    return Credential()
}

func mapping(map: Map) {

    credentialBarCode <- map["barCode"]
    credentialIsHost  <- map["isHost"]
    credentialTypeId  <- map["typeId"]
    credentialRSVP    <- map["RSVP"]
    credentialComment <- map["comments"]
    credentialZoneId  <- map["zoneId"]
    credentialMember  <- map["member"]
    credentialIsCheckedIn   <- map["isCheckIn"]
    credentialIsCheckedOut  <- map["isCheckOut"]
    credentialCountCheckIn  <- map["countCheckin"]
    credentialCountCheckOut <- map["countCheckout"]
    credentialMemberControlled  <- map["memberControlled"]
    credentialDeltaFirstCheckIn <- map["deltafirstCheckin"]
    credentialDeltaFirstCheckOut <- map["deltaFirstCheckout"]

    credentialHosts = Set(credentialHostsArray!)
    credentialHostsArray = nil

    credentialExtensionFields = Set(credentialExtensionFieldsArray!)
    credentialExtensionFieldsArray = nil

}

and I'm getting the error on this line : credentialHosts = Set(credentialHostsArray!)

Which is not that surprising because my array is declared as optional and is never initialized/filled before this line is called. so my JSON string should contain something like "credentialHosts" : [](just an empty array as long as I don't add Hosts in my app).

Maybe I'm misunderstanding some initialization part (I'm kinda new to Swift). Should I add some code to my Host class for example ? At this time it looks like this :

import Foundation
import CoreData

class Host: NSManagedObject {

@NSManaged var hostEmail: String
@NSManaged var hostId: String
@NSManaged var hostName: String
@NSManaged var hostPhone: String
@NSManaged var hostTitle: String
@NSManaged var hostCredential: Credential

}

PS : I'd like to make sure that it does not come from an misunderstanding instead of NSSet because I'm trying to use as much as possible native types in my app...

@vincent-ch
Copy link

I think you're almost there. I would change your function that way (I guessed the WS parameters' names, maybe you have to change them):

func mapping(map: Map) {

credentialBarCode <- map["barCode"]
credentialIsHost  <- map["isHost"]
credentialTypeId  <- map["typeId"]
credentialRSVP    <- map["RSVP"]
credentialComment <- map["comments"]
credentialZoneId  <- map["zoneId"]
credentialMember  <- map["member"]
credentialIsCheckedIn   <- map["isCheckIn"]
credentialIsCheckedOut  <- map["isCheckOut"]
credentialCountCheckIn  <- map["countCheckin"]
credentialCountCheckOut <- map["countCheckout"]
credentialMemberControlled  <- map["memberControlled"]
credentialDeltaFirstCheckIn <- map["deltafirstCheckin"]
credentialDeltaFirstCheckOut <- map["deltaFirstCheckout"]
credentialHostsArray <- map["credentialHosts"]
credentialExtensionFieldsArray <- map["credentialExtensionFields"]

credentialHosts = Set(credentialHostsArray!)
credentialHostsArray = nil

credentialExtensionFields = Set(credentialExtensionFieldsArray!)
credentialExtensionFieldsArray = nil

}

ObjectMapper will do the mapping of HostsArray and ExtensionFieldsArray for you. The (almost) only thing my code does is to save this mapping in a temporary array (i.e. credentialHostsArray) and then "convert it" into a NSSet/Set (i.e. credentialHosts).

@HRiffiod
Copy link

HRiffiod commented Aug 5, 2015

Thank you for your answer, I updated my code, unfortunately, same error :
capture d ecran 2015-08-05 a 10 46 34

I have a View Controller with a bunch of textfields where I get user input values, set them in a Credential object and then I want the corresponding JSON string.

Let's say my User did not specify any Host, the credentialHosts array will then be nil, according to the declaration right ?

So where using forced unwrapping, it founds nil and crashes (I guess, not 100% sure).

So maybe my mistake is I should always :

  • Create a New Credential Entity (and insert it in my managed context)
  • Retrieve the values from the text fields and set them to my new Credential
  • Create a New Host Array (even if there is no Host, just to make an empty array an avoid crashing ?)
  • Set it like this : newCredential.credentialHostsArray = hostArray
  • Then map using Mapper().toJSONString ?

@vincent-ch
Copy link

Ok, I got it! Yes, my solution works only if the parameters are always set (mandatory fields). In your case your last answer sounds good, you could also check if the arrays are nil after the mapping and then set an empty array in that case. Let me know if your solution worked!

@HRiffiod
Copy link

HRiffiod commented Aug 5, 2015

So I tried creating empty arrays, if prevents from crashing, first step :)

However, event when hard-coding some value to a host, my Json output does not contain my Host array, any idea why it does not work ?

Here is the code :

// create a host Array to avoid crashing if nil
var hostsArray = Host
let newHost = NSEntityDescription.insertNewObjectForEntityForName("Host", inManagedObjectContext: self.managedObjectContext!) as! Host
newHost.hostName = "testHostName"
hostsArray.append(newHost)
newCredential.credentialHostsArray = hostsArray

    // create a credentialExtensionFields Array to avoid crashing if nil
    var extensionFieldsArray = [ExtensionFields]()
    newCredential.credentialExtensionFieldsArray = extensionFieldsArray

    // create a request to fetch the credential
    let requestGuest = NSFetchRequest(entityName: "Credential")

    // set a predicate to retrieve the credential according to its Id
    requestGuest.predicate = NSPredicate(format: "credentialId = %@", newCredential.credentialId!)

    /**********/

    // execute request
    let testFetch = (managedObjectContext!.executeFetchRequest(requestGuest, error:nil) as! [Credential]).first!

    var jsonTestGuest = Mapper().toJSONString(testFetch, prettyPrint: true)
    println(jsonTestGuest!)

and here is the json output :

{
"memberControlled" : "Hugo",
"RSVP" : 1,
"comments" : "a comment",
"isHost" : false,
"countCheckout" : 0,
"isCheckIn" : false,
"countCheckin" : 0,
"isCheckOut" : false
}

@vincent-ch
Copy link

My solution works for a mapping from JSON and you're working to JSON.

But nevertheless, I don't understand why your hosts are empty, because you're filling the array used in the ObjectMapper's mapping process. If I understand the ObjectMapper's doc right, the <- operator works in both ways so it should work. If I have time I'll check further why it doesn't work.

Did you try to debug your code and see what happens during the mapping process?

@f1nality
Copy link

Guys, what do you if you have multiple ManagedObjectContext? I don't see any options how to tell mapper which context is should create entites in..

@marcospolanco
Copy link

@vincent-ch, you might be the only person on Earth to get this working. I followed your example (thx!) yet my code crashes calling mapping(map) with EXC_BAD_ACCESS(code=1). Seen anything like this?

@vincent-ch
Copy link

Hi guys,

I'm sorry, I don't work on that mobile project anymore, so I'll do my best to help you without XCode.

@f1nality : I set the context with the following line var ctx = DBUtils().getManagedObjectContext(). There you can define wich context you want to use. In my case, if I remembered well, the method just returns the default context.

@marcospolanco : the EXC_BAD_ACCESS happens typically when you didn't initialize an object. Here a link to help you debugging it: http://www.touch-code-magazine.com/how-to-debug-exc_bad_access/

@iroller
Copy link

iroller commented Jan 17, 2016

@bonebox how do you convert objects to JSON when using Sync? Are you still using ObjectMapper for it? Can't get them both to work together

UPDATE: nevermind, I think I figured it out 3lvis/Sync#165

@bonebox
Copy link

bonebox commented Jan 17, 2016

Yep, that's it.

@jackalcooper
Copy link

jackalcooper commented Apr 15, 2016

What is the proper place to perform the function managedObjectContext.save() ? Or it is unnecessary?

@Steveybrown
Copy link

@f1nality I have this working with multi contexts, one way to achieve this is to use a custom mapping context object. The custom mapping context object can hold an NSManagedObjectContext in which you want to save the object. To extract this NSManagedObjectContext from the init function we can do something like map.context.myManagedObjectContext. This still works cleanly with @vincent-ch solution.

@gabhisekdev
Copy link

gabhisekdev commented Oct 5, 2016

@vincent-ch Hi I am working on a project that uses objectMapper for its use. Please kindly explain what exactly is being done by this 2 functions:

override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
    super.init(entity: entity, insertIntoManagedObjectContext: DBUtils().getManagedObjectContext())
}

required init?(_ map: Map) {
    var ctx = DBUtils().getManagedObjectContext()
    var entity = NSEntityDescription.entityForName("Gallery", inManagedObjectContext: ctx)
    super.init(entity: entity!, insertIntoManagedObjectContext: ctx)

    mapping(map)
}

@tristanhimmelman
Copy link
Owner

@GABHISEKBUNTY look into the StaticMappable protocol. You should be able to integrate more easily using StaticMappable as it does not require overriding init

@gabhisekdev
Copy link

gabhisekdev commented Oct 5, 2016

@tristanhimmelman Actually I just wanted to know what exactly is going on these 2 methods. A small quick explannation will be quite helpful. And what is the need of these functions?

@tristanhimmelman
Copy link
Owner

Ah then I will leave that to @vincent-ch as I am not familiar with his implementation

@gabhisekdev
Copy link

@tristanhimmelman But what exactly this implementation helps for?

@vincent-ch
Copy link

@GABHISEKBUNTY Hi, I'm sorry, I don't work on that mobile project anymore. I cannot remember how I come to this solution. And sorry for the late answer, I was on holidays.

The first one overrides the default constructor to call the super constructor including the default context (I get it with DBUtils().getManagedObjectContext()).

The second one get the entity "Gallery" from the default context (always by calling DBUtils().getManagedObjectContext()), call the super constructor and then call the mapping method.

I hope it will help, just ask if you need some more precise informations.

@calvinsug
Copy link

calvinsug commented Jul 17, 2017

Hi @vincent-ch , is this code still working on Swift3 on iOS 10 ? cause i cant use DButils, it says "Use of unresolved identifier 'DBUtils'"

is DBUtils come from another library ?

@vincent-ch
Copy link

@calvinsug Sorry, I know this part was a little bit confusing because the DBUtils was part of my project. If I remember well, this method simply returns the default context.

@qburst-nikhila
Copy link

@vincent-ch i used your code and its working perfectly. But when i relaunch the app all the save datas in core data db get disappeared.

I used the same code

@objc(Gallery)
class Gallery: AbstractModel, Mappable {

@NSManaged var title: String?
@NSManaged var shootingDate: String?
@NSManaged var elements: NSSet?

private var elementsArray: [Image]?

override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
    super.init(entity: entity, insertIntoManagedObjectContext: DBUtils().getManagedObjectContext())
}

required init?(_ map: Map) {
    var ctx = DBUtils().getManagedObjectContext()
    var entity = NSEntityDescription.entityForName("Gallery", inManagedObjectContext: ctx)
    super.init(entity: entity!, insertIntoManagedObjectContext: ctx)

    mapping(map)
}

override func mapping(map: Map) {

    super.mapping(map)

    title           <- map["title"]
    shootingDate    <- map["shootingDate"]
    elementsArray   <- map["elements"]

    elements = NSSet(array: elementsArray!)
    elementsArray = nil

}

}

Any help will be appreciated.

@vincent-ch
Copy link

Hi @niks1990 ,
I'm sorry, I don't work on mobile projects anymore. I don't have any wise hint to suggest...

@haroldogtf
Copy link

@niks1990 could you solve your problem? I am facing the same! After relaunch I losing the data.

@billsea
Copy link

billsea commented Oct 24, 2018

Calling mapping(map) in required_init resulted in duplicates. Here's what is working for me:

import CoreData
import Foundation
import ObjectMapper

@objc(Room)
public class Room: NSManagedObject, Mappable {
private var devicesArray: [Device]?

override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: DataController.shared.persistentContainer.viewContext)
}

required public init?(map: Map) {
let ctx = DataController.shared.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "Room", in: ctx)

if let dataEntity = entity {
  super.init(entity: dataEntity, insertInto: ctx)
} else {
  super.init()
}

}

public func mapping(map: Map) {
id <- map["id"]
unit <- map["unit"]
name <- map["name"]
roomDescription <- map["roomDescription"]
devicesArray <- map["devices"]

if let devices = devicesArray {
  device = NSSet(array: devices)
}

devicesArray = nil

}
}

@richardomus
Copy link

@vincent-ch, you might be the only person on Earth to get this working. I followed your example (thx!) yet my code crashes calling mapping(map) with EXC_BAD_ACCESS(code=1). Seen anything like this?

So many years for this answer... but for me the mapping(map) crash happened when:

1 - the class prototype was missing @objc(class) declaration;
2 - the entity on .xcdatamodeld was with codegen different that "Manual/none", resulting in a colateral effect after super.init is called, an instance of the autogenerated core data class being allocated instead of my current class... bizarre.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests