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

Update 11/4/16: This tutorial has been updated for Xcode 8 and Swift 3.

In Part 1 of this tutorial, you learned about initialization in Swift and applied it by creating a launch sequence data module for a manned NASA mission to Mars. You implemented initializers for structs, tried initializer delegation, and learned when and why to use failable and throwable initializers.

But there’s more to learn, and NASA still needs your help! In this second part of the tutorial, you’ll learn about class initialization in Swift. Class initialization is very different from class initialization in Objective-C, and when writing class initializers in Swift for the first time, you’ll encounter many different compiler errors. This tutorial walks you through potential issues and gives you all the understanding necessary to avoid these compiler errors.

In this part of the tutorial, you’ll:

  • see the initializer delegation equivalent for classes,
  • learn the extra rules about failable and throwable initialization for classes,
  • learn how classes delegate initialization up the class hierarchy, and
  • learn how classes inherit initializers

Make sure to complete Part 1 first, because here you’ll build on what you learned there.

Getting Started

You can either continue working in the playground from Part 1, or create a new playground. The code in this part doesn’t depend on any code from Part 1, but Foundation is required for this tutorial, so make sure the playground imports Foundation. Importing UIKit or AppKit meets this requirement.

Initializer Delegation

Lift-off to Mars is only a year away! You’ve completed all but one very important task before you can ship the launch systems code: you need to implement models that represent different rocket components. Specifically, the tanks that hold rocket fuel and liquid oxygen.

Along the way, you’ll see the difference in how initializer delegation works with classes.

Designated Initializers

Add the RocketComponent class to the end of your playground:

class RocketComponent {
  let model: String
  let serialNumber: String
  let reusable: Bool

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

RocketComponent is another simple class. It has three constant stored properties and a designated initializer.

Remember delegating initializers from Part 1 of this tutorial? Recall that a chain of delegating initializers eventually ends by a call to a non-delegating initializer. In the world of classes, a designated initializer is just a fancy term for a non-delegating initializer. Just like with structures, these initializers are responsible for providing initial values to all non-optional stored properties declared without a default value.

The comment “// Init #1a – Designated” is not required, but as you go further in this tutorial, these types of comments will help keep your initializers organized.

At the bottom of the playground, write the following code to see the designated initializer in action:

let payload = RocketComponent(model: "RT-1", serialNumber: "234", reusable: false)

Convenience Initializers

What about delegating initializers? They have a fancy name too in the world of classes :]. Implement the following convenience initializer within RocketComponent:

// Init #1b - Convenience
convenience init(model: String, serialNumber: String) {
  self.init(model: model, serialNumber: serialNumber, reusable: false)
}

Notice how this looks just like a structure’s delegating initializer. The only difference here is that you have to prefix the declaration with the convenience keyword.

At the end of the playground, use the convenience initializer to create another instance of RocketComponent:

let fairing = RocketComponent(model: "Serpent", serialNumber: "0")

Similar to how they work with structs, convenience initializers let you have simpler initializers that just call through to a designated initializer.

Failing and Throwing Strategy

There’s an art to designing failable and throwing initializers — thankfully, it doesn’t require paint. At first, you might find yourself writing code that’s difficult to read, so below are some strategies to maximize your failable and throwing initializer legibility.

Failing and Throwing from Designated Initializers

Let’s say that all rocket components in this mission report their model and serial number as a formatted string: the model followed by a hyphen followed by the serial number. For example, “Athena-003”. Implement a new RocketComponent failable designated initializer that takes in this formatted identifier string.

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

Before moving on, instantiate the following constants to see this initializer at work:

let component = RocketComponent(identifier: "R2-D21", reusable: true)
let nonComponent = RocketComponent(identifier: "", reusable: true)

Notice in the sidebar how nonComponent is correctly set to nil because the identifier does not follow the model-serial number format.

Failing and Throwing From Convenience Initializers

Replace the initializer you just wrote with the following implementation:

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

This version is more concise.

When writing initializers, make the designated initializers non-failable and have them set all the properties. Then your convenience initializers can have the failiable logic and delegate the actual initialization to the designated initializer.

Note that there is a downside to this approach, relating to inheritance. Don’t worry, we’ll explore how to overcome this downside in the last section of this tutorial.

Subclassing

That’s all there is to know about class initialization for root classes. Root classes, which don’t inherit from other classes, are what you’ve been working with so far. The rest of this tutorial focuses on class initialization with inheritance.

Fair warning: this is where things get bumpy! Class initialization is a lot more complicated than struct initialization, because only classes support inheritance.

Differences from Objective-C

Note: If you’ve never seen Objective-C code before, don’t worry — you can still read this section!

If you’ve programmed in Objective-C, Swift class initialization will feel restrictive. Swift defines many new and not-so-obvious initialization rules with regards to classes and inheritance. Don’t be surprised if you run into unexpected compiler errors that enforce these initialization rules.

Don’t write any of this Objective-C code in your playground — you’ll hop back into your Swift playground soon enough, but first I’ll discuss Objective-C to show a naive approach to initialization.

Why are there so many rules with Swift? Consider the following Objective-C header:

@interface RocketComponent : NSObject

@property(nonatomic, copy) NSString *model;
@property(nonatomic, copy) NSString *serialNumber;
@property(nonatomic, assign) BOOL reusable;

- (instancetype)init;

@end

Assume the initializer does not set any of RocketComponent‘s properties. Objective-C will automatically initialize all properties to an empty-ish value, such as NO or 0 or nil. Bottom line: this code is capable of creating a fully initialized instance.

Note that the equivalent class with non-optional typed properties would not compile in Swift because the compiler would not know what values to use to initialize the properties. Swift does not initialize properties automatically to empty values; it only initializes optional typed properties automatically to nil. As you saw in Part 1 of this tutorial, programmers are responsible for defining initial values for all non-optional stored properties; otherwise the Swift compiler will complain.

This distinction between Objective-C and Swift initialization behavior is fundamental to understanding the long list of Swift class initialization rules. Say you update the Objective-C initializer to take initial values for each property:

@interface RocketComponent : NSObject

@property(nonatomic, copy) NSString *model;
@property(nonatomic, copy) NSString *serialNumber;
@property(nonatomic, assign) BOOL reusable;

- (instancetype)initWithModel:(NSString *)model
                 serialNumber:(NSString *)serialNumber
                     reusable:(BOOL)reusable;

@end

RocketComponent now knows how to initialize an instance without using empty values. This time, the Swift equivalent would compile successfully.