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 4 of 4 of this article. Click here to view the first page.

Overriding Using Convenience Initializers

Designated initializers are supposed to be very basic assignments of values to all stored properties, and normally take in an initial value for each property. Any time you need an initializer that simplifies initialization by having fewer arguments and/or doing some pre-processing on property values, you should make that a convenience initializer. That’s why they’re called convenience initializers, after all: they make initialization more convenient than using a designated initializer.

In the previous section, you overrode RocketComponent‘s designated initializer with a designated initializer in Tank. Think about what this override is doing: it is overriding RocketComponent‘s designated initializers to make it more convenient to initialize Tank instances. Initialization is more convenient because this override does not require a value for encasingMaterial.

When overriding a superclass designated initializer, you can either make it a designated initializer or a convenience initializer. To turn the overridden initializers in LiquidTank into convenience initializers, replace the code for initializers #3d and #3e with:

// Init #3d - Convenience
convenience override init(model: String, serialNumber: String, reusable: Bool) {
  self.init(model: model, serialNumber: serialNumber, reusable: reusable,
    encasingMaterial: "Aluminum", liquidType: "LOX")
}

// Init #3e - Convenience
convenience override init(model: String, serialNumber: String, reusable: Bool,
    encasingMaterial: String) {
  self.init(model: model, serialNumber: serialNumber,
    reusable: reusable, encasingMaterial: "Aluminum")
}

There is one downside to overriding superclass designated initializers with subclass designated initializers. If the subclass designated initializer has logic, you can’t delegate to it from a convenience initializer. Instead, you can override a superclass’s designated initializer with a convenience initializer; this allows you to delegate to the subclass’s designated initializer logic. You’ll try this next.

Failing and Throwing Strategy With Inheritance

Add the following convenience initializer to LiquidTank:

// Init #3f - Convenience
convenience init?(identifier: String, reusable: Bool, encasingMaterial: String,
    liquidType: String) {
  let identifierComponents = identifier.components(separatedBy: "-")
  guard identifierComponents.count == 2 else {
    return nil
  }
  self.init(model: identifierComponents[0], serialNumber: identifierComponents[1],
    reusable: reusable, encasingMaterial: encasingMaterial, liquidType: liquidType)
}

This initializer looks almost identical to RocketComponent‘s convenience initializer.

Instantiate a new LiquidTank using this new convenience initializer:

let athenaFuelTank = LiquidTank(identifier: "Athena-9", reusable: true,
  encasingMaterial: "Aluminum", liquidType: "RP-1")

This works, but there’s duplicate code in LiquidTank‘s and in RocketComponent‘s initializers. Duplicate code introduces the possibility for bugs, so this just won’t do.

No one wants bugs on Mars.

No one wants bugs on Mars.

To follow the DRY (Don’t Repeat Yourself) principle, add the following type method to RocketComponent:

static func decompose(identifier: String) ->
    (model: String, serialNumber: String)? {
  let identifierComponents = identifier.components(separatedBy: "-")
  guard identifierComponents.count == 2 else {
    return nil
  }
  return (model: identifierComponents[0], serialNumber: identifierComponents[1])
}

This code refactors out the duplicate code from the initializers and places it in a single place accessible to both initializers.

Note: Type methods are marked with the static keyword. They are called on the type itself instead of an instance. You might not have thought of it this way before, but calling a method inside the implementation of a class is really calling an instance method. For example, self.doSomething(). You need to use a type method in this situation because decompose(identifier:) as an instance method would not be available until phase 2, after all properties have been initialized. You’re using decompose(identifier:) as a utility to calculate a property value for initialization. I recommend playing around with calling methods in initializers of your own classes to get the hang of this concept.

Now update both convenience initializers from RocketComponent and LiquidTank to call the new static method:

// Init #1c - Convenience
convenience init?(identifier: String, reusable: Bool) {
  guard let (model, serialNumber) = RocketComponent.decompose(identifier: identifier) else {
    return nil
  }
  self.init(model: model, serialNumber: serialNumber, reusable: reusable)
}

and:

// Init #3f - Convenience
convenience init?(identifier: String, reusable: Bool, encasingMaterial: String,
    liquidType: String) {
  guard let (model, serialNumber) = RocketComponent.decompose(identifier: identifier) else {
    return nil
  }
  self.init(model: model, serialNumber: serialNumber, reusable: reusable,
    encasingMaterial: encasingMaterial, liquidType: liquidType)
}

This is a great way to take advantage of failable convenience initializers while removing any redundancy. You can still have the initializer return nil as needed, and you’ve refactored the common code into the type method to avoid repeating yourself.

Where to Go From Here?

Whew, that was a lot to cover! You’ve learned a ton about initialization in this two-parter tutorial … and thanks to you, the first manned mission to Mars is ready for lift-off. Great job!

If you want to compare code or just want a look at the final code, the complete playground for Part 2 can be downloaded here.

To review what you’ve learned here and to read about all the initialization rules and compiler safety checks, download Apple’s book The Swift Programming Language.

To get more practice and to see initialization in the wider context of structs and classes, check out our book, Swift Apprentice. We also have a great tutorial on Object-Oriented Design in Swift which touches on initialization.

Now that Swift is open source, you can find lots of interesting documents related to initialization in the Swift GitHub repo under the docs folder. By reading these documents you can learn the Swift team’s justification for initialization features and safety checks. Follow the Swift-evolution and Swift-evolution-announce mailing lists to get a glimpse of what’s on the horizon.

We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!