realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.32k stars 2.15k forks source link

Support custom RLMObject initializers in Swift #1101

Closed maciejtrybilo closed 9 years ago

maciejtrybilo commented 10 years ago
class Foo: RLMObject {
    dynamic var bar: String

    init(bar: String) {
        self.bar = bar

        super.init()
    }
}

let foo = Foo(bar: "😿")

The last line results in fatal error: use of unimplemented initializer 'init()' for class 'moduleName.Foo'. Somehow Realm seems to want to call the default initializer, but ideally I wouldn't want to implement it because I don't necessarily have reasonable default values and would rather avoid optionals.

tgoyne commented 10 years ago

We currently require default initializers for Swift classes due to limitations with Swift introspection. String properties are reported as being of type id due to that it's not an Objective-C type, and the only solution for that we've found is to create an object of the class and inspect the runtime type of id fields. This does unfortunately mean that all properties need a default (and in the case of String, it can't be nil) even if it's not a meaningful default.

mrh-is commented 9 years ago

@maciejtrybilo If all of the properties have default values, you can define an init() that just calls super.init(). The framework creates an instance to inspect the types but doesn't use it otherwise. After that, everything works as you would expect, so your code can call your custom init method. Hope that helps for now!

Pintouch commented 9 years ago

@mrh-is I tried defining the default constructor as you mention, I'm getting fatal error: use of unimplemented initializer 'init(objectSchema:)' for class Foo.

class Foo: RLMObject {
    dynamic var bar = ""
    override init() {
        super.init()
    }
    init(bar: String) {
        self.bar = bar

        super.init()
    }
}

let foo = Foo(bar: "😿")

3 months passed since you commented it, so maybe it's not working anymore since new Xcode version? Is there a workaround ?

mrh-is commented 9 years ago

@Pintouch Yep, it's a change sometime between 0.87.4 and 0.90.0. Still investigating...

Edit: It's between 0.89.2 and 0.89.0. Still investigating even more.

mrh-is commented 9 years ago

OK, I'm not totally certain, because I (sadly!) can't spend the whole work day on debugging this so I didn't test with this commit, but I think it's commit 94c2dee. My suspicion (again, untested, sorry) is that there's some issue with isSwift being applied to the whole chain rather than decided per class. I think someone with more familiarity with the schema loader will need to look into this, I'm at the limits of my abilities.

But definitely a breaking change for the Swift side of things. (Or is there a better way to set up Swift classes with custom initializers?)

jpsim commented 9 years ago

This change happened in #1284 and lays the groundwork for a super awesome new Swift API, but makes using Realm in Swift a bit more cumbersome in the meantime :cry:.

Until our super awesome new Swift API is released, you'll unfortunately have to implement the following initializers if you want to use custom initializers:

init() {
  super.init()
}
init(object:) {
  super.init(object:object)
}
init(object:schema:) {
  super.init(object: object, schema: schema)
}
init(objectSchema:) {
  super.init(objectSchema: objectSchema)
}
jpsim commented 9 years ago

I understand this is suuuuper awkward, but as I said, this lays an important foundation.

mrh-is commented 9 years ago

Ah, that's ok! As long as there are clear directions. :+1: Thanks @jpsim! (YEAH SWIFT API)

Pintouch commented 9 years ago

Thanks! Overriding those constructors works! Can't wait for the Swift API with generics Support!! :)

xzf158 commented 9 years ago

class Sentence: RLMObject { convenience init(content: String){ self.init(object: content) } } This is working for me. http://quabr.com/27294702/how-do-i-fix-this-error-use-of-unimplemented-initializer-init-for-class

loganwright commented 9 years ago

Just in case anyone else comes here that is beginning Realm for the first time and doesn't know the argument types, here's a drop in solution for RLMObjects with custom initializers. Just add this to your class

    // MARK: RLMSupport

    /*
    Initializers required for RLM Support -- https://github.com/realm/realm-cocoa/issues/1101
    */

    override init(object: AnyObject?) {
        super.init(object:object)
    };
    override init(object value: AnyObject!, schema: RLMSchema!) {
        super.init(object: value, schema: schema)
    }
    override init(objectSchema: RLMObjectSchema) {
        super.init(objectSchema: objectSchema)
    }
jdelaune commented 9 years ago

Will we know when the new Swift API hits? I assume it hasn't yet

tgoyne commented 9 years ago

It's now merged to master if you want to play around with it. We'll make an official announcement once it's included in a release.

weixiyen commented 9 years ago

Nice! when is the anticipated release? Roughly within 2 weeks?

wimhaanstra commented 9 years ago

It would be awesome if the podspec could be updated to include the Swift release. Makes it a lot easier to test it out :)

One of the things I really don't like is specifying default values for my properties because some of them are required and now this isn't enforced at compile-time. Also, adding the initializers mentioned here will not work when you work with non-optional properties without a default value.

jpsim commented 9 years ago

@depl0y we'll be writing a new podspec for RealmSwift. Subscribe to #1705 to know when that's done.

One of the things I really don't like is specifying default values for my properties because some of them are required and now this isn't enforced at compile-time. Also, adding the initializers mentioned here will not work when you work with non-optional properties without a default value.

Default values aren't strictly necessary, although calling init() is required to succeed. Aside from that, you can create any number of initializers in any format you like, as long as it calls super.init() at some point.

wimhaanstra commented 9 years ago

@jpsim yup, but you can't create a custom init, if there are required properties.

For example:

class MyOwnClass: RLMObject {

    dynamic var on: Bool = false;
    dynamic var someRelation: MyOtherClass

    init(someRelationParameter: MyOtherClass) {

        self.someRelation = someRelationParameter;
        super.init();

    }
}

This throws an error without a initialiser called init(). So then you create a simple initialiser, like this:

override init() {
    super.init();
}

Of course you get a compile error here, because you are calling super.init() before the property someRelation is set. So should I set the someRelation property just to MyOtherClass()?

override init() {
    self.someRelation = MyOtherClass();
    super.init();
}

Finally you end up with a bunch of initialisers (override init(), override init!(objectSchema schema: RLMObjectSchema!) and your custom initialiser). And this leads to all kinds of bugs, like objects being created with default values.

I created an example:

class DemoObject1: RLMObject {

    dynamic var someObject: DemoObject2;

    override init() {
        self.someObject = DemoObject2();
        super.init();
    }

    override init!(objectSchema schema: RLMObjectSchema!) {
        self.someObject = DemoObject2();
        super.init(objectSchema: schema);
    }

    init(someObject: DemoObject2) {
        self.someObject = someObject;

        super.init();
    }

}

class DemoObject2: RLMObject {

    dynamic var name: String = "";

}

Now, what should be the appropriate way to init a DemoObject2, init a DemoObject1 and commit them to the database?

I tried:

RLMRealm.defaultRealm().beginWriteTransaction()
var demo2 = DemoObject2()
demo2.name = "MyDemoObject"
RLMRealm.defaultRealm().addObject(demo2);

var demo1 = DemoObject1(someObject: demo2)
RLMRealm.defaultRealm().addObject(demo1);
RLMRealm.defaultRealm().commitWriteTransaction();

But this results in 2 DemoObject2 objects being stored, one with a name and the other without. The DemoObject1 is stored in the database, but the someObject property is always pointing to a record without the name value set.

I tried splitting it up in multiple transactions, but this ends in the same problem. (This might even be a separate thread, sorry about that).

Convenience initialisers do work for me, without problems, but this has the need that all properties are either optional or filled with a default value.

yoshyosh commented 9 years ago

Another example of a working init custominit

jpsim commented 9 years ago

@yoshyosh are you sure init() and init!(objectSchema schema: RLMObjectSchema!) are necessary at all here? All they do is call super.

tgoyne commented 9 years ago

They're needed for the default property values to work when the other initializers are called from obj-c. It's (probably) a swift bug.

lbanders commented 9 years ago

We have run in to similar problems, but it seems to be gone in 0.91.3 and Swift 1.2. In fact the build failed when we had the override init() methods.

jpsim commented 9 years ago

We'll be releasing a Swift-specific optimized API in the very near future, which will make using custom initializers in Swift much friendlier.

acoleman-apc commented 9 years ago

If this is required, why is it not mentioned in the swift docs located here: https://realm.io/docs/swift/latest/#models

It seems to me that the models section needs a init subsection with a copy of the latest / working examples listed here . . . .

jpsim commented 9 years ago

@acoleman-apc this issue was last active over 4 months ago and is no longer relevant.

To override an Object initializer, just add it to your model:

public class MyModel: Object {
    required public init() {
        // custom initialization logic
        super.init()
    }
}
acoleman-apc commented 9 years ago

@jpsim Thanks but I have already added similar code to my project. The point was that this isn't mentioned in the current documentation but is instead in an issue that has been open since November of 2014. At this point, doesn't it deserve a mention in the official documentation?

jpsim commented 9 years ago

This issue is actually not open, but rather has been closed for over 4 months.

However, as with anything you may be struggling with in Realm, we're open to updating our docs in order to clarify. But we generally avoid documenting standard language behavior like overriding parent class methods or adding custom methods, as seems to be the case here.

frankradocaj commented 9 years ago

@jpsim I'm kind of a newbie with Swift (hey, aren't we all?). Documentation in the main website stating that you have to implement the following would be very helpful in getting started with Realm (aka "the happy path" 😀).

To create a custom constructor:

import Realm
import RealmSwift

class MyModel: Object {

    // Make sure to declare this constructor if you wanna have a custom one like below
    required init() { 
        super.init()
    }

    // And this one too
    required override init(realm: RLMRealm, schema: RLMObjectSchema) {
        super.init(realm: realm, schema: schema)
    }

    // Now go nuts creating your own constructor
    init(myCustomValue: String) {
        ...
    }
}
DrJid commented 9 years ago

+1 to @frankradocaj I'm a newbie to Realm and this was actually quite frustrating. So a mention in the docs for someone just getting started would be really nice. Was going to skip on realm because it seemed harder than it should be.

kharmabum commented 8 years ago

+1 on including this in the docs. Especially an example with relations.

art-divin commented 8 years ago

+1 for adding this into the "happy path" reference part. -3 hours.

ed-mejia commented 8 years ago

+1 @frankradocaj for the example, that should be on the main documentation :tired_face:

jpsim commented 8 years ago

@ed-mejia we have a section in our docs on this: https://realm.io/docs/swift/latest/#adding-custom-initializers-to-object-subclasses

ed-mejia commented 8 years ago

Hey @jpsim thanks for your comment.

In my case it did't work just with that, maybe because I need other init since Im using a parsing Json library, look at my actual model:

import Foundation
import Realm
import RealmSwift
import Gloss

public final class RoleModel: Object, Decodable {

    dynamic var roleId = 0
    dynamic var title = ""
    dynamic var updatedAt: NSDate? = NSDate()

    override public static func primaryKey() -> String? {
        return "roleId"
    }

    public init?(json: JSON) {
        super.init()
        // check for required values in order to return a valid object
        guard let roleId: Int = "role_id" <~~ json, let title: String = "title" <~~ json else {
            return nil
        }

        self.roleId = roleId
        self.title = title
        self.updatedAt = Decoder.decodeDate("updated_at")(json)
    }

    // MARK: - Required initializers to make Realm works with custom inits
    required public init() {
        super.init()
    }

    required override public init(realm: RLMRealm, schema: RLMObjectSchema) {
        super.init(realm: realm, schema: schema)
    }
}

In my case the compiler requires me to implement _required public init()_

_public init(realm: RLMRealm, schema: RLMObjectSchema)_ is not required by the compiler but the App crash when I try to do this after a query:

let contents = realm.objects(RoleModel)
print("from DB: \(contents.count)")
print(contents[0]) //<- it crash here

And the crash indicates that I haven't implemented _public init(realm: RLMRealm, schema: RLMObjectSchema)_

kishikawakatsumi commented 8 years ago

@ed-mejia It is more simple to use public convenience init?(json: JSON) instead public init?(json: JSON). If you implement the convenience initializer, the compiler doesn't require to implement other initializers.

Like the following (Change public init? to public convenience init? and super.init() to self.init()):

public final class RoleModel: Object, Decodable {
    dynamic var roleId = 0
    dynamic var title = ""
    dynamic var updatedAt: NSDate? = NSDate()

    override public static func primaryKey() -> String? {
        return "roleId"
    }

    public convenience init?(json: JSON) {
        self.init()
        // check for required values in order to return a valid object
        guard let roleId: Int = "role_id" <~~ json, let title: String = "title" <~~ json else {
            return nil
        }

        self.roleId = roleId
        self.title = title
        self.updatedAt = Decoder.decodeDate("updated_at")(json)
    }
}
ed-mejia commented 8 years ago

@kishikawakatsumi You are totally right :+1: thanks for that clarification :smile:

rameez-leftshift commented 8 years ago

Hey guys have the same issue but not able to resolve it by following the above steps. Can you help me with it??

ed-mejia commented 8 years ago

@rameez-leftshift Please explain your case, attach examples and maybe we could help you ..

rameez-leftshift commented 8 years ago

Here is my class which is causing the crash

public class StoryBoard: Object, Deserializable {

public dynamic var status: Status?

required public init() {
    status = Status()
    super.init()
}

required public init(data: [String: AnyObject]) {
    let new_id: Int = data["id"]! as! Int
    id =  "\(new_id)"
    super.init()
}

public class func fromJSON(data: [String : AnyObject]) -> StoryBoard? {
    return StoryBoard(data: data)
}    
}

let me know if you need anything else

ed-mejia commented 8 years ago

Without knowing what's your crash about.

could you please try this first, replace your init this way:

public convenience init(data: [String: AnyObject]) {
        self.init()
// PUT ALL your initialisation here and remember to delete super.init()
}
rameez-leftshift commented 8 years ago

This is the error which i get : use of unimplemented initializer 'init(realm:schema:)' for class 'App.StoryBoard'.

Your mentioned solution doesn't work. Thank you for the help though. Also i'm using swift 2.1.1 and realm version 0.97.4.

ed-mejia commented 8 years ago

Weird, that's the same error I got previously and it's working fine with the provided solution,

try to put this in your class:

// MARK: - Required initializers to make Realm works with custom inits
    required public init() {
        super.init()
    }

And let's see if you can avoid the crash... this should not be the final solution though.

rameez-leftshift commented 8 years ago

i already have the at the start of the code snippet, its immediately after my declarations.

rameez-leftshift commented 8 years ago

Have created a new question for my problem (https://github.com/realm/realm-cocoa/issues/3185).

jhoughjr commented 8 years ago

I'm noticing the docs on custom initializers aren't working for me.

import Foundation
import RealmSwift

class Session: Object {

    dynamic var uuid: String = NSUUID().UUIDString
    dynamic var startTime: NSTimeInterval
    dynamic var endTime: NSTimeInterval

    convenience init(startDate:NSDate) {
        self.init()
        self.startTime = startDate.timeIntervalSince1970

    }

    func spanString() -> String {
        var returnString = ""

        return returnString
    }

    override class func primaryKey() -> String {
        return "uuid"
    }
}

The compile error is : Missing argument for parameter 'startDate' It occurs at the self.init() call. I used the code example in the docs section on custom initializers. Any ideas?

kharmabum commented 8 years ago

Try setting initial values for both startTime and endTime.

On Mar 12, 2016, 8:40 PM -0800, Jimmy Hough Jr.notifications@github.com, wrote:

I'm noticing the docs on custom initializers aren't working for me. `import Foundation import RealmSwift

class Session: Object {

dynamic var uuid: String = NSUUID().UUIDString dynamic var startTime: NSTimeInterval dynamic var endTime: NSTimeInterval convenience init(startDate:NSDate) { self.init() self.startTime = startDate.timeIntervalSince1970 } func spanString() ->String { var returnString = "" return returnString } override class func primaryKey() ->String { return "uuid" }

}`

The compile error is : Missing argument for parameter 'startDate' It occurs at the self.init() call. I used the code example in the docs section on custom initializers. Any ideas?

— Reply to this email directly orview it on GitHub(https://github.com/realm/realm-cocoa/issues/1101#issuecomment-195874431).

jhoughjr commented 8 years ago

So from reading this and the linked issue, I infer that Realm simply cannot support optionals and convenience initializers. Is that the case?

jpsim commented 8 years ago

So from reading this and the linked issue, I infer that Realm simply cannot support optionals and convenience initializers. Is that the case?

That's not the case, see Realm's documentation on custom initializers: https://realm.io/docs/swift/latest#adding-custom-initializers-to-object-subclasses

bdkjones commented 3 years ago

I realize this is an old thread, but it would be REALLY handy if stuff like this were documented right up front for Realm. Important details like, "You have to use the default init() method on Object subclasses" is something to tell folks right away, in the "Welcome to your first Realm project" tutorials. Otherwise, you spend hours writing your own init() methods only to get a crash and then dig through GitHub issues to discover yet another arcane limitation of Realm.

Edit: It's also complicated because there are SO MANY different docs for Realm. There's the legacy stuff, the new MongoDB stuff, the individual SDK docs, etc. Googling for help with Realm leads to outdated sources and incorrect information.

xingheng commented 1 year ago

It's mid-2023, and Realm 10.33 still has the same issue.