Programming in Swift: Functions & Types

Jan 4 2022 · Swift 5.5, iOS 15, Xcode 13

Part 5: Protocols & Inheritance

40. Initializers

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 39. Challenge: Inheritance Next episode: 41. Challenge: Initializers

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Notes: 40. Initializers

Update Notes: This course was originally recorded in 2019. It has been reviewed and all content and materials updated as of October 2021.

Transcript: 40. Initializers

You’ve already learned how to write basic initializers. You use them to set any values needed by your types at initialization time! But when inheritance is in the mix, you’ll need to incorporate a few more rules when writing your initializers.

We’ll start with this code, which is a simplified version of what we went over in the previous exercise on inheritance. A Student is a Person with grades, and a Student Athlete is a Student …currently with no specialization other than the type name. Let’s add a stored “sports” property, using a String Array.

47 var sports: [String]

We’re not giving it a default value, so you might have expected these errors. And, you might have thought to write an initializer, to silence them. Perhaps just like the one in Person, only with another parameter for sports.

49 init(firstName: String, lastName: String, sports: [String]) {

}

You will be able to take care of the errors, with this initializer, but because StudentAthlete is a subclass, you’ll have to perform your initialization in a certain order.

First, you’ll need to give sports a value.

50 self.sports = sports

And only after that, can you pass the values to the superclass’s initializer. That’s done by calling super.init, to access the initializer defined all the way up in the base class, Person.

51 super.init(firstName: firstName, lastName: lastName)

And now we just need to give our student athlete instance a sports array, using this new initializer.

  StudentAthlete(...... sports: ["Foosball"])

Swift has a requirement that all stored properties have initial values. So not only does a subclass have to ensure that its own stored properties are initialized, but it has to make sure that the stored properties of its superclass hierarchy are also initialized. And only by using super.init, at the end of the process, can that be done.

Let’s have a closer look at Swift’s two-phase initialization, for class hierarchies.

In phase one, you must initialize all of the stored properties for the class instance. And that process flows from the bottom of the class hierarchy, to the top.

So in this example, inside the initializer for StudentAthlete, you initialize its own stored property (self.sports), and then call the superclass’s initializer, which also has to initialize stored properties (firstName and LastName).

Then this chain repeats. Each superclass class initializes its own stored properties, and then calls super.init. You can’t use instance properties and methods until phase one is complete.

But afterwards, you can. That’s “Phase 2”, where you can use anything that requires the use of “self”, before returning from the initializer.

Two-phase initialization is a helpful Swift feature. In other languages without it, interacting with properties before they’ve been initialized, can cause buggy behavior. In Swift, you can feel safe knowing that after you call super.init, your object is ready to use, with clearly-defined initial values.

Back in our example, we’ve now got an initializer that explicitly requires a “sports” Array, but what we don’t have is the original initializer, from Student, that only takes a first and last name.

StudentAthlete(firstName: "Bernie", lastName: "Kosar")

Initializers can’t be automatically inherited the same way that methods and properties are. That’s because there’s no guarantee that all properties would be initialized. In this case, sports wouldn’t have a value. The simplest solution would just be to assign a default value to the sports array.

49 … sports: [String] = []) {

But as we’ll get into shortly, that’s not always going to solve your initializer inheritance problems. So let’s learn the alternatives.

49 … sports: [String]) {

First, you can override an initializer. And as you saw earlier, for overriding, it’s easiest to just start typing the name of what you want to override, and then autocomplete.

54 override init(firstName: String, lastName: String) {

}

Here, you can just copy and paste the code from the other StudentAthlete initializer…

55 self.sports = sports
   super.init(firstName: firstName, lastName: lastName)

…except, starting off with sports being empty.

55 … = []

Now: Person, Student, and StudentAthlete all have access to this latest initializer. But StudentAthlete can only use it because you decided to override it.

But sometimes, you’re going to be required to implement an initializer, for an entire class hierarchy. Often, that’s going to be the case when you need satisfy a protocol – which we haven’t gone into yet. For now, you’ll learn how to about to deal with required initializers. And later, you can use the technique for whatever reason you might need it.

You can enforce that all subclasses of a type must implement a certain initializer, by using the required keyword,

36 required init…

You’ll get one error by doing that, which you can solve with a Fix-It.

54 required init…

When you implement a required initializer, you don’t use the override keyword. The concept of overriding is “built into” required.

And if you’re wondering if you could rewrite this required initializer, to have a default argument for sports, that’s a good question.

49 //  init(firstName: String, lastName: String, sports: [String]) {
//    self.sports = sports
//    super.init(firstName: firstName, lastName: lastName)
//  }
  
  required init(firstName: String, lastName: String, sports: [String] = []) {

Unfortunately, the answer is no. To the Swift compiler, that doesn’t count.

49 init(firstName: String, lastName: String, sports: [String]) {
    self.sports = sports
    super.init(firstName: firstName, lastName: lastName)
  }
  
  required init(firstName: String, lastName: String) {

What you’re looking at now are the two forms of what are called “designated initializers”. One is not required to be there, and the other is, but they both need to call super.init.

But there’s another way to express the same code, with only one designated initializer. We’ll start that, by, instead of calling super.init, in the required initializer, calling self.init.

57 self.init(firstName: <#T##String#>, lastName: <#T##String#>, sports: <#T##[String]#>)

There, forward firstName and lastName along, as well as an empty sports array.

57 self.init(firstName: firstName, lastName: lastName, sports: [])

That takes care of the same thing as the two lines of code above it, so delete them.

54 required init(firstName: String, lastName: String) {
    self.init(firstName: firstName, lastName: lastName, sports: [])
  }

And then Xcode knows what to do. Have it perform a Fix-It.

54 required convenience init(firstName: String, lastName: String) {

Now, the keyword convenience is added, which is what allows you to call self.init, instead of super.init.

A “convenience initializer” is the alternative to a “designated initializer”. Instead of handling the initialization of stored properties itself, a “convenience initializer” delegates that work to a designated initializer.

You’re not skipping any steps of the process. Your code does eventually need to result in a call super.init. You just don’t do that directly, in a convenience initializer.

You just experienced one use a convenience initializer: to satisfy the requirement of a certain initializer signature being present. But that’s not all they’re for. Convenience initializers are great for adding new –convenient!– ways to instantiate an object.

For example, let’s say that we wanted to be able to create a new Student, based on someone transferring to a new school. You could do that with a convenience initializer that accepted a transfer Student as its parameter.

45 convenience init(transfer: Student) {

}

You’d use self.init, to forward their name along.

46 self.init(firstName: transfer.firstName, lastName: transfer.lastName)

At that point, grades would have the default value (an empty array), so you’d reassign it to be the transfer’s grades.

47 grades = transfer.grades

That’s possible because after self.init, you’re working in phase two of initialization, where variable properties can be mutated.

But it looks like we’ve inherited the required initializer from Person, in Student. That might seem a bit confusing. Do initializers get inherited or don’t they?!

There’s not a simple “yes” or “no” answer, but there are clear rules about it.

If you haven’t written your own designated initializer, for a subclass, the superclass designated initializers will be inherited. Of course, this requires that any stored properties of the subclass are given default values. Which is what Student did, with its grades.

Also, if your subclass implements all of the designated initializers of its superclass, then it gets to inherit all of convenience initializers of the superclass.

So, we can transfer a student athlete, without writing any other code.

36 let rudy = StudentAthlete(firstName: "Daniel"…
…
39 StudentAthlete(transfer: rudy)

However, their sports won’t transfer.

39 …rudy).sports

Let’s follow the initializer chain, to find out why.

On line 39, we’re calling the convenience initializer defined for Student, which is on line 17.

On line 18, because of polymorphism, the self.init call is actually initializing a StudentAthlete, on line 26.

And there, you can see the empty sports array. Which gets passed along as we’ve already gone over.

So unfortunately, if we want the sports to transfer, we’ll need another convenience initializer, for StudentAthlete.

65  convenience init(transfer: StudentAthlete) {
    
  }

Then, we can transfer their name, and sports, using the designated initializer.

self.init(firstName: transfer.firstName, lastName: transfer.lastName, sports: transfer.sports)

And just as with the convenience initializer for Student, we’ll transfer their grades.

grades = transfer.grades

Now, you can see that Rudy’s sport has transferred over.

Here’s a summary of the compiler rules for using designated and convenience initializers:

A designated initializer must call a designated initializer from its immediate superclass. So for example, a designated initializer in StudentAthlete must call a designated initializer from Student - it can’t skip up the chain to Person.

Even if you don’t have a designated initializer explicitly defined, this chain is still happening. Student doesn’t have an explicit designated initializer, but Person, its superclass, does. Student always initializes its own grades and then forwards initialization up to Person.

A convenience initializer must call another initializer from the same class. That could be a designated initializer, or another convenience initializer. But whatever the case, self.init means “initialize THIS class”. Not the superclass.

Lastly, a convenience initializer must ultimately call a designated initializer. You could have a chain of convenience initializers, one calling the next, and have it be as long as you’d like. But eventually, the chain must end with a designated initializer.

If you comprehend all that, you’ve got a fantastic understanding of inheritance in Swift. But we went over a lot. You don’t need to have all of those rules memorized in order to program in Swift. Feel free to continue, and use this video as a reference if and when you need to!