Why Convenience Keyword Is Even Needed in Swift

What is the difference between convenience init vs init in swift, explicit examples better

Standard init:

Designated initializers are the primary initializers for a class. A
designated initializer fully initializes all properties introduced by
that class and calls an appropriate superclass initializer to continue
the initialization process up the superclass chain.

convenience init:

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. You can
also define a convenience initializer to create an instance of that class
for a specific use case or input value type.

per the Swift Documentation

In a nutshell, this means that you can use a convenience initializer to make calling a designated initializer faster and more "convenient". So convenience initializers require the use of self.init instead of the super.init you might see in an override of a designated initializer.

pseudocode example:

init(param1, param2, param3, ... , paramN) {
// code
}

// can call this initializer and only enter one parameter,
// set the rest as defaults
convenience init(myParamN) {
self.init(defaultParam1, defaultParam2, defaultParam3, ... , myParamN)
}

I use these a lot when creating custom views and such that have long initializers with mainly defaults. The docs do a better job explaining than I can, check them out!

What are convenience required initializers in Swift?

A convenience required initializer is an initializer that is enforced onto all subclasses but is not the designated initializer. This means that said initializer will eventually call a designated initializer in its initialization chain.

Designated Initialisers

A designated initialiser is the canonical initializer for a class and the one which all required and convenience initialisers should call. The Docs say:

Designated initializers are the primary initializers for a class. A
designated initializer fully initializes all properties introduced by
that class and calls an appropriate superclass initializer to continue
the initialization process up the superclass chain.

Convenience Initialisers

A convenience initialiser is an initializer that sets up certain configuration information on a class...conveniently. Documentation:

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.
You can also define a convenience initializer to create an instance of
that class for a specific use case or input value type.

You do not have to provide convenience initializers if your class does
not require them. Create convenience initializers whenever a shortcut
to a common initialization pattern will save time or make
initialization of the class clearer in intent

Required Initialisers

Required initializers can be thought of as a binding contract between a parents interface and subsequent subclasses. Its your means of enforcing that all your children are aware of and implement a certain set of initialisers.

Write the required modifier before the definition of a class
initializer to indicate that every subclass of the class must
implement that initializer:

Does Convenience Init need to have same number of parameters as Required Init

There is no correlation in number of parameters between designated (or even required) initializers and convenience initializers.

Convenience initializers are simply those that need to forward initialization to one of designated initializers. How many input parameters will it have is completely dependent on implementation and use-case. It may be more, less or equal.

It is hard to find a simple-enough example to demonstrate all of those but consider something like this:

class HighlightedWordContainer {

let words: [String]
var highlightedWord: String

init(words: [String], highlighted: String) {
self.words = words
self.highlightedWord = highlighted
}

init(words: [String], highlightedIndex: Int) {
self.words = words
self.highlightedWord = words[highlightedIndex]
}

convenience init(singleWord: String) {
self.init(words: [singleWord], highlighted: singleWord)
}

convenience init(word1: String, word2: String, word3: String, highlighted: String) {
self.init(words: [word1, word2, word3], highlighted: highlighted)
}

convenience init(wordsSeparatedByWhitespace: String, highlightedIndex: Int) {
let words = wordsSeparatedByWhitespace.components(separatedBy: .whitespaces)
self.init(words: words, highlightedIndex: highlightedIndex)
}

convenience init?(descriptor: [String: Any], keys: [String], selectedKey: String) {
let words: [String] = keys.compactMap { descriptor[$0] as? String }
guard words.isEmpty == false else { return nil }

guard let selectedWord = descriptor[selectedKey] as? String else { return nil }
guard let selectedWordIndex = words.firstIndex(of: selectedWord) else { return nil }

self.init(words: words, highlightedIndex: selectedWordIndex)
}

}

Here I created a class with 2 designated initializers. These two need to set all properties that are not already set by default. Which means they need to set both words and highlightedWord. A designated initializer may not delegate a call to another designated initializer so the following will not work:

init(words: [String], highlightedIndex: Int) {
self.init(words: words, highlighted: words[highlightedIndex])
}

And all convenience initializers do need to call any of the designated initializers and may also not directly set or use self properties UNTIL a designated constructor is being called. So:

convenience init(wordsSeparatedByWhitespace: String, highlightedIndex: Int) {
let words = wordsSeparatedByWhitespace.components(separatedBy: .whitespaces)
// print(highlightedWord) // NOT OK!
self.init(words: words, highlightedIndex: highlightedIndex)
print(highlightedWord)
}

And from the example I hope it makes clear that a convenience initializer can have more, fewer or same number of input parameters. It all just depends.

Some more plausible examples:

class Point {
let x: Int
let y: Int

init(x: Int, y: Int) { self.x = x; self.y = y }
convenience init(x: Int) { self.init(x: x, y: 0) }
convenience init(y: Int) { self.init(x: 0, y: y) }
convenience init(polar: (radius: Double, angle: Double)) { self.init(x: Int(cos(polar.angle)*polar.radius), y: Int(sin(polar.angle)*polar.radius)) }
}

class NumericValueAsString {
let stringValue: String // A value represented as "123.456"

init(stringValue: String) { self.stringValue = stringValue }
convenience init(value: Int) { self.init(stringValue: .init(value)) }
convenience init(integerPart: String, fractionPart: String) { self.init(stringValue: integerPart + "." + fractionPart) }
}

Also; required keyword has nothing to do with anything in this context. You can place it to any of the initializers (designated or convenience), to multiple of them or even all of them.

Adding additional info based on a comment:

your examples shows that you can have a convenience init that can pass
parameters back to the required init``. what about instances where I
there is no convenience init``` that can pass a value to parameter to
one which is in required init but not in the convenience init. Like my
speed parameter in my original question.

This really depends on the interface of your class which is not fully shown in your example. For instance, is speed publicly exposed?

If speed is public then what you are looking for is something along the lines

let itemInstance = MyItem()
itemInstance.timeStamp = timeStamp
itemInstance.position = position
itemInstance.distance = distance
itemInstance.speed = speed

or

let itemInstance = MyItem(timeStamp: timeStamp, position: position, distance: distance)
itemInstance.speed = speed

or even create a new convenience constructor

extension MyItem {
convenience init(timeStamp: FitTime? = nil,
position: Position? = nil,
distance: Measurement<UnitLength>? = nil
speed: Speed? = nil
) {
self.init(timeStamp: timeStamp, position: position, distance: distance)
self.speed = speed
}
}

let itemInstance = MyItem(timeStamp: timeStamp, position: position, distance: distance, speed: speed)

But if your speed property is privately defined than you can not assign to it anywhere but internally. You need to be able to modify your class so that it can accept this property somewhere. Either via constructor, via property or via a method. Until there is no access at all you can not expect to be able to modify it.

But if this is your own class I would turn things upside down if possible. Your main constructor should be the one that accepts all 4 parameters. But your convenience initializer should be required (Yes, convenience initializer may be required). So in that case you would have:

public required convenience init() {
self.init(timeStamp: self, position: self, distance: self, speed: self)
}

public init(timeStamp: FitTime? = nil,
position: Position? = nil,
distance: Measurement<UnitLength>? = nil,
speed: Speed?) {
super.init()

self.timeStamp = timeStamp
self.position = position
self.distance = distance
self.speed = speed
}
}

This specific case feels like it is missing something compared to your code but again, you did not provide full definition of your class and with it does or is supposed to do. For instance in the code you provided if one would call a convenience initializer then the following lines would execute in this order:

self.$timeStamp.owner = self // from self.init()
self.timeStamp = timeStamp

which looks like a problem to begin with.

So to wrap it up. There are very many ways you can achieve what you are asking for. But it all depends on what you are trying to build here.

I hope it at least puts you in the right track.

Why we should go for convenience Initializers than manual initializers

In simple cases like your example there isn't a big difference in using convenience init or manually setting the property.

Convenience initialisers come in handy when after setting your properties you have to perform some complicated logic and don't want to duplicate your code.

This

class A {

var name:String;
var salary:Float;

init(name:String,salary:Float) {
self.name = name;
self.salary = salary;

//hundreds of line of very complicated logic in order to perform important stuff
}

convenience init(name:String) {
self.init(name:name , salary:8000.0)
}

}

is more readable, and easier to understand and manage for future changes, than this

class B {

var name:String;
var salary:Float;

init(name:String,salary:Float) {
self.name = name;
self.salary = salary;

//hundreds of lines of very complicated logic in order to perform important stuff
}

init(name:String) {
self.name = name;
self.salary = 8000.0;

//the same, repeated, hundreds of lines as before of very complicated logic in order to perform the same important stuff
}

}

What's the difference between a required initializer and a designated initializer?

Required initialisers and designated initialisers are not really related, though the associated keywords required and convenience are both used to specify restrictions on subclasses.

Required Initialisers

A required initialiser makes a guarantee that you can initialise a type, or any of its sub-types, with that initialiser. If you have an initialiser in a protocol and you conform something to that protocol, you have to use required (if it's a class) because that protocol guarantees that the initialiser is present on that class, and any of its subclasses. When you use required on an initialiser of a class, that signals that all of its subclasses can also be initialised using that method. This means you also need to add that initialiser to any of its subclasses.

protocol TestProtocol {
init()
}

class TestClass: TestProtocol {
required init() {

}
}

Here, the required keyword must be present because any subclasses of TestClass must also provide init() (because they also conform to TestProtocol).

Having a required initialiser allows you to initialise a class without knowing what it is at compile time, which is useful for a variety of reasons:

let classType: TestProtocol.Type = TestClass.self
let object = classType.init()

If your class conformed to multiple protocols, each with a different initialiser for example, each of those initialisers must also be required:

protocol OtherProtocol {
init(thing: Int)
}

class OtherClass: TestClass, OtherProtocol {
let thing: Int

required init() { // Required from superclass/its protocol
self.thing = 0
}

required init(thing: Int) { // Required from new protocol
self.thing = thing
}
}

Note that adding super.init() isn't required in this special case, because Swift will automatically include the call if it takes no parameters.

In all the above examples, the initialisers are designated because they do not include the convenience keyword.

Even if you didn't have any protocols, you can still make use of required by initialising a type of a class which isn't known at compile time:

class BaseClass {
let value: Int

required init(value: Int) {
self.value = value
}
}

class SubClass: BaseClass {
required init(value: Int) { // Required from superclass
super.init(value: value) // Must call desginated initialiser of superclass
}
}

let someBaseClassType: BaseClass.Type = SubClass.self
let someBaseClassInstance = someBaseClassType.init(value: 1)

Designated Initialisers

A designated initialiser is one which isn't a convenience initialiser (i.e, marked with convenience). A designated initialiser must make sure that all properties of the class have a value before the initialiser finishes (or a super initialiser is called). Convenience initialisers only don't have this requirement because they must themselves call a designated initialiser.

class OtherSubClass: BaseClass {
convenience required init(value: Int) {
self.init() // Must call designated initialiser of this class
}

init() {
super.init(value: 0) // Must call designated initialiser of superclass
}
}

(This is fairly contrived example.)

In my experience, convenience initialisers are rarely useful and I tend to find the problems they solve can be solved using optional arguments on designated initialisers instead. One also needs to consider the fact that initialisers can't call convenience initialisers on their superclass, so make sure you don't have any convenience initialisers which provide functionality that your designated initialisers don't if you intend your class to be subclassed!


Structs and enums don't use the required or convenience keywords because these keywords are both used to indicate initialisation rules for subclasses, which only classes support: The required keyword indicates that subclasses must provide that initialiser, and the convenience keyword indicates that subclasses cannot call that initialiser. Despite not having the keywords, they must still provide initialisers defined in any protocols they conform to, and you can write 'convenient' initialisers which call self.init, just without the convenience keyword.


To respond to your statements:

  • Required initialisers don't have to be designated.
  • Designated initialisers don't have to be required.
  • Classes can have multiple required and designated initialisers.

Why this convenience init doesn't introduce ambiguity in Swift?

A function that doesn't take an argument is considered a better match than a function with an argument with a default value. Compare:

func f(_ arg: String = "") { print("unary") }
func f() { print("nullary") }
f()

Output:

nullary

It is ambiguous if both functions have arguments with default values:

func f(_ arg: String = "") { print("string") }
func f(_ arg: Int = 1) { print("int") }
f()

Output:

error: Untitled Page 2.xcplaygroundpage:4:1: error: ambiguous use of 'f'
f()
^

Untitled Page 2.xcplaygroundpage:2:6: note: found this candidate
func f(_ arg: String = "") { print("string") }
^

Untitled Page 2.xcplaygroundpage:3:6: note: found this candidate
func f(_ arg: Int = 1) { print("int") }
^


Related Topics



Leave a reply



Submit