Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convenience initializer with non-optional property

An object of mine has an integer ID. Since this is a required property I am not defining it as an optional and I am requiring it in the designated initializer:

class Thing {

    var uniqueID: Int
    var name: String?

    init (uniqueID: Int) {
        self.uniqueID = uniqueID
    }       

}

Since I am creating one of these from some JSON, the usage is along the lines of:

if let uniqueID = dictionary["id"] as? Int {
    let thing = Thing(uniqueID: unique)
}

Next, I would like to be able to add a convenience initializer to the Thing class that accepts the dictionary object and sets the properties accordingly. This includes the required uniqueID and some other optional properties. My best effort so far is:

convenience init (dictionary: [String: AnyObject]) {
    if let uniqueID = dictionary["id"] as? Int {
        self.init(uniqueID: uniqueID)
        //set other values here?
    }
        //or here?
}

But of course this isn't sufficient since the designated initializer isn't called on all paths of the conditional.

How should I be handling this scenario? Is it even possible? Or should I accept that uniqueID must be an optional?

like image 314
Ben Packard Avatar asked Mar 16 '15 21:03

Ben Packard


People also ask

What is a convenience initializer?

A convenience initializer is a secondary initializer that must call a designated initializer of the same class. It is useful when you want to provide default values or other custom setup. A class does not require convenience initializers.

What must a convenience initializer call in Swift?

Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer's parameters set to default values.

How do you designate a Failable initializer?

You write a failable initializer by placing a question mark after the init keyword ( init? ). Note: You cannot define a failable and a nonfailable initializer with the same parameter types and names. A failable initializer creates an optional value of the type it initializes.

Which type of property is initialized when it is accessed first in Swift?

Example: Swift Initializer Inside the initializer, we have initialized the value of the length property. Here, when the wall1 object is created, the init() initializer is called.


2 Answers

You have a couple of options with this one. One is a failable initialisers:

convenience init?(dictionary: [String: AnyObject]) {
    if let uniqueID = dictionary["id"] as? Int {
        self.init(uniqueID: uniqueID)
    } else {
        self.init(uniqueID: -1)
        return nil
    }
}

Technically this can be tweaked a bit (mainly depending on your preference/version of swift), but my person preference is something as follows:

class func fromDictionary(dictionary: [String: AnyObject]) -> Thing? {
    if let uniqueID = dictionary["id"] as? Int {
        return self.init(uniqueID: uniqueID)
    }

    return nil
}

All together, as a playground:

class Thing {
    var uniqueID: Int
    var name: String?

    init(uniqueID: Int) {
        self.uniqueID = uniqueID
    }

    convenience init?(dictionary: [String: AnyObject]) {
        if let uniqueID = dictionary["id"] as? Int {
            self.init(uniqueID: uniqueID)
        } else {
            self.init(uniqueID: -1)
            return nil
        }
    }

    class func fromDictionary(dictionary: [String: AnyObject]) -> Thing? {
        if let uniqueID = dictionary["id"] as? Int {
            return self.init(uniqueID: uniqueID)
        }

        return nil
    }
}

let firstThing = Thing(uniqueID: 1)
let secondThing = Thing(dictionary: ["id": 2])
let thirdThing = Thing(dictionary: ["not_id": 3])
let forthThing = Thing.fromDictionary(["id": 4])
let fithThing = Thing.fromDictionary(["not_id": 4])
like image 146
Joseph Duffy Avatar answered Oct 28 '22 00:10

Joseph Duffy


The best solution is probably to use a failable initializer, which will either return an instantiated object or nil.

Because Swift objects cannot be partially constructed and convenience initializers must call a non-convenience initializer, we must still do something in the failure case.

The result will look something like this:

convenience init?(dictionary: [String: AnyObject]) {
    if let uniqueID = dictionary["id"] as? Int {
        self.init(uniqueID: uniqueID)
    } else {
        self.init(uniqueID: 0)
        return nil
    }
}

Generally speaking, our non-convenience initializer(s) should be one that accepts all arguments, and convenience initializers should be methods which don't require some of the arguments.

For example, I might make my default initializer look like this:

init(uniqueID: Int, name: String? = nil) {
    self.uniqueID = uniqueID
    self.name = name
}

This allows us to call the constructor in several different ways:

let thing1 = Thing(1)
let thing2 = Thing(2, nil)
let thing3 = Thing(3, "foo")
let thing4 = Thing(4, myUnwrappedStringVar)
let thing5 = Thing(5, myWrappedStringOptional)

And that already covers a lot of use cases for us.

So, let's add another convenience initializer that accepts an optional Int.

convenience init?(uniqueID: Int? = nil, name: String? = nil) {
    if let id = uniqueID {
        self.init(uniqueID: id, name: name)
    } else {
        self.init(uniqueID: 0)
        return nil
    }
}

Now we can take an Int? for our uniqueID argument and just fail when it's nil.

So, one more to accept the dictionary.

convenience init?(dictionary: [String: AnyObject]) {
    let uniqueID = dictionary["id"] as? Int
    let name = dictionary["name"] as? String
    self.init(uniqueID: uniqueID, name: name)
}

We still have the slightly weird initialize then return nil pattern in our first convenience constructor, but everything else we build on top of this can simply call that convenience initializer and doesn't require the weird pattern.

In the initializer that takes the dictionary, if there's no id key, or if it's something that's not an Int, then the let uniqueID will be nil, so when we call the other constructor, it will call the one that accepts an Int?, be passed nil, return nil, and therefore the one we called will return nil.

like image 28
nhgrif Avatar answered Oct 27 '22 23:10

nhgrif