Swift Tutorial: Initialization In Depth, Part 2/2

Launch your Swift skills to the next level as you continue your study of initialization to cover class initialization, subclasses, and convenience initializers. By René Cacheaux.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Adding Properties to Subclasses

Check out the following RocketComponent Objective-C subclass and its instantiation:

@interface Tank : RocketComponent

@property(nonatomic, copy) NSString *encasingMaterial;

@end

Tank *fuelTank = [[Tank alloc] initWithModel:@"Athena" serialNumber:@"003" reusable:YES];

Tank introduces a new property, but does not define a new initializer. That’s okay, because the new property will be initialized to nil per Objective-C behavior. Notice how fuelTank is a Tank initialized by the initializer implemented in RocketComponent, the superclass. Tank inherits initWithModel:serialNumber:reusable: from RocketComponent.

This is where things really start to break down in Swift. To see it, write in your playground the equivalent Tank subclass from above using Swift. Note that this code will not compile.

class Tank: RocketComponent {
  let encasingMaterial: String
}

Notice how this subclass introduces a new stored property, encasingMaterial. This code does not compile because Swift does not know how to fully initialize an instance of Tank. Swift needs to know what value should be used to initialize the new encasingMaterial property.

You have three options to fix the compiler error:

  1. Add a designated initializer that calls or overrides the designated initializer for the superclass RocketComponent.
  2. Add a convenience initializer that calls the designated initializer for the superclass RocketComponent.
  3. Add a default value for the stored property.

Let’s go for option 3 since it’s the simplest. Update the Tank subclass by declaring “Aluminum” as the default property value for encasingMaterial:

class Tank: RocketComponent {
  let encasingMaterial: String = "Aluminum"
}

It compiles and runs! Not only that, but your effort is rewarded with a bonus: you can take advantage of the initializers inherited from RocketComponent without adding one to Tank.

Using Inherited Initializers

Instantiate a tank with this code:

let fuelTank = Tank(model: "Athena", serialNumber:"003", reusable: true)
let liquidOxygenTank = Tank(identifier: "LOX-012", reusable: true)

That’s an easy way of solving the missing initializer compiler error. This works just as it would in Objective-C. However, most of the time your subclasses will not automatically inherit initializers from their superclasses. You will see this in action later.

Understanding the impact of adding stored properties within a subclass is critical to avoiding compiler errors. In preparation for the next section, comment out the fuelTank and liquidOxygenTank instantiations:

// let fuelTank = Tank(model: "Athena", serialNumber:"003", reusable: true)
// let liquidOxygenTank = Tank(identifier: "LOX-012", reusable: true)

Adding Designated Initializers to Subclasses

encasingMaterial from Tank is a constant property with a default value of "Aluminum". What if you needed to instantiate another tank that is not encased in Aluminum? To accommodate this requirement, remove the default property value and make it a variable instead of a constant:

var encasingMaterial: String

The compiler errors out with, “Class ‘Tank’ has no initializers”. Every subclass that introduces a new non-optional stored property without a default value needs at least one designated initializer. The initializer should take in the initial value for encasingMaterial in addition to initial values for all the properties declared in the RocketComponent superclass. You have already built designated initializers for root classes, and it’s time to build one for a subclass.

Let’s build out Option 1 from the “Adding properties to subclasses” section: add a designated initializer that calls or overrides the designated initializer for the superclass RocketComponent.

Your first impulse might be to write the designated initializer like this:

init(model: String, serialNumber: String, reusable: Bool, encasingMaterial: String) {
  self.model = model
  self.serialNumber = serialNumber
  self.reusable = reusable
  self.encasingMaterial = encasingMaterial
}

This looks like all the designated initializers you have built throughout this tutorial. However, this code won’t compile, because Tank is a subclass. In Swift, a subclass can only initialize properties it introduces. Subclasses cannot initialize properties introduced by superclasses. Because of this, a subclass designated initializer must delegate up to a superclass designated initializer to get all of the superclass properties initialized.

Add the following designated initializer to Tank:

// Init #2a - Designated
init(model: String, serialNumber: String, reusable: Bool, encasingMaterial: String) {
  self.encasingMaterial = encasingMaterial
  super.init(model: model, serialNumber: serialNumber, reusable: reusable)
}

The code is back to compiling successfully! This designated initializer has two important parts:

  1. Initialize the class’s own properties. In this case, that’s just encasingMaterial.
  2. Delegate the rest of the work up to the superclass designated initializer, init(model:serialNumber:reusable:).

Two-Phase Initialization Up a Class Hierarchy

Recall that two-phase initialization is all about making sure delegating initializers do things in the correct order with regards to setting properties, delegating and using a new instance. So far you’ve seen this play out for struct delegating initializers and for class convenience initializers, which are essentially the same.

There’s one more kind of delegating initializer: the subclass designated initializer. You built one of these in the previous section. The rule for this is super easy: you can only set properties introduced by the subclass before delegation, and you can’t use the new instance until phase 2.

To see how the compiler enforces two-phase initialization, update Tank‘s initializer #2a as follows:

// Init #2a - Designated
init(model: String, serialNumber: String, reusable: Bool, encasingMaterial: String) {
  super.init(model: model, serialNumber: serialNumber, reusable: reusable)
  self.encasingMaterial = encasingMaterial
}

This fails to compile because the designated initializer is not initializing all the stored properties this subclass introduces during phase 1.

Update the same initializer like this:

// Init #2a - Designated
init(model: String, serialNumber: String, reusable: Bool, encasingMaterial: String) {
  self.encasingMaterial = encasingMaterial
  self.model = model + "-X"
  super.init(model: model, serialNumber: serialNumber, reusable: reusable)
}

The compiler will complain that model is not variable. Don’t actually do this, but if you were to change the property from a constant to a variable, you would then see this compiler error:

This errors out because the subclass designated initializer is not allowed to initialize any properties not introduced by the same subclass. This code attempts to initialize model, which was introduced by RocketComponent, not Tank.

You should now be well equipped to recognize and fix this compiler error.

To prepare for the next section, update Tank's 2a initializer to how it was before this section:

// Init #2a - Designated
init(model: String, serialNumber: String, reusable: Bool, encasingMaterial: String) {
  self.encasingMaterial = encasingMaterial
  super.init(model: model, serialNumber: serialNumber, reusable: reusable)
}