Some things are inherently awesome: rockets, missions to Mars, initialization in Swift. This tutorial combines all three for a combo-platter of awesomeness in which you’ll get to learn about the power of initialization!
Initialization in Swift is about what happens when you create a new instance of a named type:
let number = Float() |
Initialization is the time to manage the inital values of stored properties for named types: classes, structures, and enumerations. Because of the safety features built into Swift, initialization can be tricky. There are a lot of rules, some of which are not obvious.
By following this two-part tutorial, you will learn the ins and outs to designing initializers for your Swift types. In Part 1, you’ll begin with the basics including structure initialization, and in Part 2 you’ll move on to learning about class initialization.
Before getting started, you should be familiar with the basics of initialization in Swift and be comfortable with concepts such as optional types, throwing and handling errors, and declaring default stored property values. Also, make sure you have Xcode 7.2 or later installed.
If you need a refresher on the basics, or if you are just starting to learn Swift, check out our book Swift Apprentice or our many Swift intro tutorials.
Getting Started
Let’s set the scene: it’s your first day on your new job as a launch software engineer at NASA (go you!). You’ve been tasked with designing the data model that will drive the launch sequence for the first manned mission to Mars, Mars Unum. Of course, the first thing you do is convince the team to use Swift. Then …
Open Xcode and create a new playground named BlastOff. You can select any platform, since the code in this tutorial is platform-agnostic and depends only on Foundation.
Throughout the tutorial, remember this one golden rule: You cannot use an instance until it is fully initialized. “Use” of an instance includes accessing properties, setting properties and calling methods. Everything in Part 1 applies specifically to structures unless otherwise specified.
Banking on the Default Initializer
To start modeling the launch sequence, declare a new structure named RocketConfiguration
in your playground:
struct RocketConfiguration { } |
Below the closing curly brace of the definition for RocketConfiguration
, initialize a constant instance named athena9Heavy
:
let athena9Heavy = RocketConfiguration() |
This uses a default initializer to instantiate athena9Heavy
. In the default initializer, the name of the type is followed by empty parentheses. You can use default initializers when your types either don’t have any stored properties, or all of the type’s stored properties have default values. This holds true for both structures and classes.
Add the following three stored properties inside the struct definition:
let name: String = "Athena 9 Heavy" let numberOfFirstStageCores: Int = 3 let numberOfSecondStageCores: Int = 1 |
Notice how the default initializer still works. The code continues to run because all the stored properties have default values. That means the default initializer doesn’t have very much work to do since you’ve provided defaults!
What about optional types? Add a variable stored property named numberOfStageReuseLandingLegs
to the struct definition:
var numberOfStageReuseLandingLegs: Int? |
In our NASA scenario, some of the rockets are reusable, while others are not. That’s why numberOfStageReuseLandingLegs
is an optional Int
. The default initializer continues to run fine because optional stored property variables are initialized to nil
by default. However, that’s not the case with constants.
Change numberOfStageReuseLandingLegs
from a variable to a constant:
let numberOfStageReuseLandingLegs: Int? |
Notice how the playground reports a compiler error:
You won’t run into this often, since constant optionals are rarely needed. To fix the compiler error, assign a default value of nil
to numberOfStageReuseLandingLegs
:
let numberOfStageReuseLandingLegs: Int? = nil |
Hooray! The compiler is happy again, and initialization succeeds. With this setup, numberOfStageReuseLandingLegs
will never have a non-nil value. You cannot change it after initialization, since it is declared as a constant.
Banking on the Memberwise Initializer
Rockets are usually made up of several stages, which is the next thing to model. Declare a new struct named RocketStageConfiguration
at the bottom of the playground:
struct RocketStageConfiguration { let propellantMass: Double let liquidOxygenMass: Double let nominalBurnTime: Int } |
This time, you have three stored properties propellantMass
, liquidOxygenMass
and nominalBurnTime
with no default values.
Create an instance of RocketStageConfiguration
for the rocket’s first stage:
let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1, liquidOxygenMass: 276.0, nominalBurnTime: 180) |
None of RocketStageConfiguration
‘s stored properties have default values. Also, there is no initializer implemented for RocketStageConfiguration
. Why isn’t there a compiler error? Swift structures (and only structures) automatically generate a memberwise initializer. This means you get a ready-made initializer for all the stored properties that don’t have default values. This is super handy, but there are several gotchas.
Imagine you submit this snippet for code review and your developer team lead tells you all properties should be ordered alphabetically.
Update the RocketStageConfiguration
to re-order the stored properties:
struct RocketStageConfiguration { let liquidOxygenMass: Double let nominalBurnTime: Int let propellantMass: Double } |
What happened? The stageOneConfiguaration
initializer call is no longer valid, because the automatic memberwise initializer argument list’s order mirrors that of the stored property list. Be careful, because when re-ordering structure properties, you might break instance initialization. Thankfully the compiler should catch the error, but it is definitely something to watch out for.
Undo the stored property re-order change to get the playground compiling and running again:
struct RocketStageConfiguration { let propellantMass: Double let liquidOxygenMass: Double let nominalBurnTime: Int } |
All your rockets burn for 180 seconds, so it’s not useful to pass the nominal burn time every time you instantiate a stage configuration. Set nominalBurnTime
‘s default property value to 180:
let nominalBurnTime: Int = 180 |
Now there’s another compiler error:
Compilation fails because memberwise initializers only provide parameters for stored properties without default values. In this case, the memberwise initializer only takes in propellant mass and liquid oxygen mass, since there is already a default value for burn time.
Remove nominalBurnTime
‘s default value so that there is no compiler error.
let nominalBurnTime: Int |
Next, add a custom initializer to the struct definition that provides a default value for burn time:
init(propellantMass: Double, liquidOxygenMass: Double) { self.propellantMass = propellantMass self.liquidOxygenMass = liquidOxygenMass self.nominalBurnTime = 180 } |
Notice that the same compiler error is back on stageOneConfiguration
!
Wait, shouldn’t this work? All you did was provide an alternative initializer, but the original stageOneConfiguration
initialization should work because it’s using the automatic memberwise initializer. This is where it gets tricky: you only get a memberwise initializer if a structure does not define any initializers. As soon as you define an initializer, you lose the automatic memberwise initializer.
In other words, Swift will help you out to start. But as soon as you add your own initializer, it assumes you want it to get out of the way.
Remove the nominalBurnTime
argument from stageOneConfiguration
‘s initialization:
let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1, liquidOxygenMass: 276.0) |
All is good again! :]
But what if you still need the automatic memberwise initializer? You can certainly write the equivalent initializer, but that’s a lot of work. Instead, move the custom initializer into an extension
before you instantiate an instance.
Your struct will now be in two parts: the main definition, and an extension with your two-parameter initializer:
struct RocketStageConfiguration { let propellantMass: Double let liquidOxygenMass: Double let nominalBurnTime: Int } extension RocketStageConfiguration { init(propellantMass: Double, liquidOxygenMass: Double) { self.propellantMass = propellantMass self.liquidOxygenMass = liquidOxygenMass self.nominalBurnTime = 180 } } |
Notice how stageOneConfiguration
continues to initialize successfully with two parameters. Now re-add the nominalBurnTime
parameter to stageOneConfiguration
‘s initialization:
let stageOneConfiguration = RocketStageConfiguration(propellantMass: 119.1, liquidOxygenMass: 276.0, nominalBurnTime: 180) |
That works too! If the main struct definition doesn’t include any initializers, Swift will still automatically generate the default memberwise initializer. Then you can add your custom ones via extensions to get the best of both worlds.
Implementing a Custom Initializer
Weather plays a key role in launching rockets, so you’ll need to address that in the data model. Declare a new struct named Weather
as follows:
struct Weather { let temperatureCelsius: Double let windSpeedKilometersPerHour: Double } |
The struct
has stored properties for temperature in degrees Celsius and wind speed in kilometers per hour.
Implement a custom initializer for Weather
that takes in temperature in degrees Fahrenheit and wind speed in miles per hour. Add this code below the stored properties:
init(temperatureFahrenheit: Double, windSpeedMilesPerHour: Double) { self.temperatureCelsius = (temperatureFahrenheit - 32) / 1.8 self.windSpeedKilometersPerHour = windSpeedMilesPerHour * 1.609344 } |
Defining a custom initializer is very similar to defining a method, because an initializer’s argument list behaves exactly the same as a method’s. For example, you can define a default argument value for any of the initializer parameters.
Change the definition of the initializer to:
init(temperatureFahrenheit: Double = 72, windSpeedMilesPerHour: Double = 5) { ... |
Now if you call the initializer with no parameters, you’ll get some sensible defaults. At the end of your playground file, create an instance of Weather
and check its values:
let currentWeather = Weather() currentWeather.temperatureCelsius currentWeather.windSpeedKilometersPerHour |
Cool, right? The default initializer uses the default values provided by the custom initializer. The implementation of the custom initializer converts the values into metric system equivalents and stores the values. When you check the values of the stored properties in the playground sidebar, you’ll get the correct values in degrees Celsius (22.2222) and kilometers per hour (8.047).
An initializer must assign a value to every single stored property that does not have a default value, or else you’ll get a compiler error. Remember that optional variables automatically have a default value of nil
.
Next, change currentWeather
to use your custom initializer with new values:
let currentWeather = Weather(temperatureFahrenheit: 87, windSpeedMilesPerHour: 2) |
As you can see, custom values work just as well in the initializer as default values. The playground sidebar should now show 30.556 degrees and 3.219 km/h.
That’s how you implement and call a custom initializer. Your weather struct is ready to contribute to your mission to launch humans to Mars. Good work!
Avoiding Duplication with Initializer Delegation
It’s time to think about rocket guidance. Rockets need fancy guidance systems to keep them flying perfectly straight. Declare a new structure named GuidanceSensorStatus
with the following code:
struct GuidanceSensorStatus { var currentZAngularVelocityRadiansPerMinute: Double let initialZAngularVelocityRadiansPerMinute: Double var needsCorrection: Bool init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Bool) { let radiansPerMinute = zAngularVelocityDegreesPerMinute * 0.01745329251994 self.currentZAngularVelocityRadiansPerMinute = radiansPerMinute self.initialZAngularVelocityRadiansPerMinute = radiansPerMinute self.needsCorrection = needsCorrection } } |
This struct holds the rocket’s current and initial angular velocity for the z-axis (how much it’s spinning). The struct also keeps track of whether or not the rocket needs a correction to stay on its target trajectory.
The custom initializer holds important business logic: how to convert degrees per minute to radians per minute. The initializer also sets the initial value of the angular velocity to keep for reference.
You’re happily coding away when the guidance engineers show up. They tell you that a new version of the rocket will give you an Int
for needsCorrection
instead of a Bool
. The engineers say a positive integer should be interpreted as true
, while zero and negative should be interpreted as false
. Your team is not ready to change the rest of the code yet, since this change is part of a future feature. So how can you accommodate the guidance engineers while still keeping your structure definition intact?
No sweat — add the following custom initializer below the first initializer:
init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Int) { let radiansPerMinute = zAngularVelocityDegreesPerMinute * 0.01745329251994 self.currentZAngularVelocityRadiansPerMinute = radiansPerMinute self.initialZAngularVelocityRadiansPerMinute = radiansPerMinute self.needsCorrection = (needsCorrection > 0) } |
This new initializer takes an Int
instead of a Bool
as the final parameter. However, the needsCorrection
stored property is still a Bool
, and you set correctly according to their rules.
After you write this code though, something inside tells you there must be a better way. There’s so much repetition of the rest of the initializer code! And if there’s a bug in the calculation of the degrees to radians conversion, you’ll have to fix it in multiple places — an avoidable mistake. This is where initializer delegation comes in handy.
Replace the initializer you just wrote with the following:
init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Int) { self.init(zAngularVelocityDegreesPerMinute: zAngularVelocityDegreesPerMinute, needsCorrection: (needsCorrection > 0)) } |
This initializer is a delegating initializer and, exactly as it sounds, it delegates initialization to another initializer. To delegate, just call any other initializer on self
.
Delegate initialization is useful when you want to provide an alternate initializer argument list but you don’t want to repeat logic that is in your custom initializer. Also, using delegating initializers helps reduce the amount of code you have to write.
To test the initializer, instantiate a variable named guidanceStatus
:
let guidanceStatus = GuidanceSensorStatus(zAngularVelocityDegreesPerMinute: 2.2, needsCorrection: 0) guidanceStatus.currentZAngularVelocityRadiansPerMinute // 0.038 guidanceStatus.needsCorrection // false |
The playground should compile and run, and the two values you checked for the guidanceStatus
properties will be in the sidebar.
One more thing — you’ve been asked to provide another initializer that defaults needsCorrection
to false. That should be as easy as creating a new delegating initializer and setting the needsCorrection
property inside before delegating initialization. Try adding the following initializer to the struct, and note that it won’t compile.
init(zAngularVelocityDegreesPerMinute: Double) { self.needsCorrection = false self.init(zAngularVelocityDegreesPerMinute: zAngularVelocityDegreesPerMinute, needsCorrection: self.needsCorrection) } |
Compilation fails because delegating initializers cannot actually initialize any properties. There’s a good reason for this: the initializer you are delegating to could very well override the value you’ve set, and that’s not safe. The only thing a delegating initializer can do is manipulate values that are passed into another initializer.
Knowing that, remove the new initializer and give the needsCorrection
argument of the main initiaziler a default value of false
:
init(zAngularVelocityDegreesPerMinute: Double, needsCorrection: Bool = false) { |
Update guidanceStatus
‘s initialization by removing the needsCorrection
argument:
let guidanceStatus = GuidanceSensorStatus(zAngularVelocityDegreesPerMinute: 2.2) guidanceStatus.currentZAngularVelocityRadiansPerMinute // 0.038 guidanceStatus.needsCorrection // false |
Way to go! Now you can put those DRY (Don’t Repeat Yourself) principles into practice.
Introducing Two-Phase Initialization
So far, the code in your initializers have been setting up your properties and calling other initializers. That’s the first phase of initialization, but there are actually two phases to initializing a Swift type.
Phase 1 starts at the beginning of initialization and ends once all stored properties have been assigned a value. The remaining initialization execution is phase 2. You cannot use the instance you are initializing during phase 1, but you can use the instance during phase 2. If you have a chain of delegating initializers, phase 1 spans the call stack up to the non-delegating initializer. Phase 2 spans the return trip from the call stack.
Putting Two-Phase Initialization to Work
Now that you understand two-phase initialization, let’s apply it to our scenario. Each rocket engine has a combustion chamber where fuel is injected with oxidizer to create a controlled explosion that propels the rocket. Setting up these parameters is the phase 1 part to prepare for blastoff.
Implement the following CombustionChamberStatus
struct to see Swift’s two-phase initialization in action. Make sure to show Xcode’s Debug area to see the output of the print statements.
struct CombustionChamberStatus { var temperatureKelvin: Double var pressureKiloPascals: Double init(temperatureKelvin: Double, pressureKiloPascals: Double) { print("Phase 1 init") self.temperatureKelvin = temperatureKelvin self.pressureKiloPascals = pressureKiloPascals print("CombustionChamberStatus fully initialized") print("Phase 2 init") } init(temperatureCelsius: Double, pressureAtmospheric: Double) { print("Phase 1 delegating init") let temperatureKelvin = temperatureCelsius + 273.15 let pressureKiloPascals = pressureAtmospheric * 101.325 self.init(temperatureKelvin: temperatureKelvin, pressureKiloPascals: pressureKiloPascals) print("Phase 2 delegating init") } } CombustionChamberStatus(temperatureCelsius: 32, pressureAtmospheric: 0.96) |
You should see the following output in the Debug Area:
Phase 1 delegating init Phase 1 init CombustionChamberStatus fully initialized Phase 2 init Phase 2 delegating init |
As you can see, phase 1 begins with the call to the delegating initializer init(temperatureCelsius:pressureAtmospheric:)
during which self
cannot be used. Phase 1 ends right after self.pressureKiloPascals
gets assigned a value in the non-delegating initializer. Each initializer plays a role during each phase.
Isn’t the compiler super crazy smart? It knows how to enforce all these rules. At first, those rules might seem like nuisances, but remember that they provide a ton of safety.
What if Things Go Wrong?
You’ve been told the launch sequence will be fully autonomous, and that the sequence will perform a ton of tests to make sure all systems are good to go for launch. If an invalid value is passed into an initializer, the launch system should be able to know and react.
There are two ways to handle initialization failures in Swift: using failable initializers, and throwing from an initializer. Initialization can fail for many reasons, including invalid input, a missing system resource such as a file, and possible network failures.
Using Failable Initializers
There are two differences between normal initializers and failable initializers. One is that failable initializers return optional values, and the other is that failable initializers can return nil
to express an initialization failure. This can be very useful — let’s apply it to the rocket’s tanks in our data model.
Each rocket stage carries two large tanks; one holds fuel, while the other holds oxidizer. To keep track of each tank, implement a new struct named TankStatus
as follows:
struct TankStatus { var currentVolume: Double var currentLiquidType: String? init(currentVolume: Double, currentLiquidType: String?) { self.currentVolume = currentVolume self.currentLiquidType = currentLiquidType } } let tankStatus = TankStatus(currentVolume: 0.0, currentLiquidType: nil) |
There’s nothing wrong with this code except that it doesn’t recognize failure. What happens if you pass in a negative volume? What if you pass in a positive volume value but no liquid type? These are all failure scenarios. How can you model these situatons using failable initializers?
Start by changing TankStatus
‘s initializer to a failable initializer by appending a ?
to init
:
init?(currentVolume: Double, currentLiquidType: String?) { |
Option-click on tankStatus
and notice how the initializer now returns an optional TankStatus
.
Update tankStatus
‘s instantiation to match the following:
if let tankStatus = TankStatus(currentVolume: 0.0, currentLiquidType: nil) { print("Nice, tank status created.") // Printed! } else { print("Oh no, an initialization failure occured.") } |
The instantiation logic checks for failure by evaluating whether the returned optional contains a value or not.
Of course, there’s something missing: the initializer isn’t actually checking for invalid values yet. Update the failable initializer to the following:
init?(currentVolume: Double, currentLiquidType: String?) { if currentVolume < 0 { return nil } if currentVolume > 0 && currentLiquidType == nil { return nil } self.currentVolume = currentVolume self.currentLiquidType = currentLiquidType } |
As soon as an invalid input is detected, the failable initializer returns nil
. You can return nil
at any time within a structure’s failable initializer. This is not the case with a class’s failable initializer, as you’ll see in Part 2 of this tutorial.
To see instantiation failure, pass an invalid value into tankStatus
‘s instantiation:
if let tankStatus = TankStatus(currentVolume: -10.0, currentLiquidType: nil) { |
Notice how the playground prints, “Oh no, an initialization failure occurred.” Because initialization failed, the failable initializer returned a nil value and the if let
statement executed the else
clause.
Throwing From an Initializer
Failable initializers are great when returning nil
is an option. For more serious errors, the other way to handle failure is throwing from an initializer.
You have one last structure to implement: one to represent each astronaut. Start by writing the following code:
struct Astronaut { let name: String let age: Int init(name: String, age: Int) { self.name = name self.age = age } } |
The manager tells you an astronaut should have a non-empty String
for his or her name
property and should have an age
ranging from 18 to 70.
To represent possible errors, add the following error enumeration before the implementation of Astronaut
:
enum InvalidAstronautDataError: ErrorType { case EmptyName case InvalidAge } |
The enumeration cases here cover the possible problems you might into when initializing a new Astronaut
instance.
Next, replace the the Astronaut
initializer with the following implementation:
init(name: String, age: Int) throws { if name.isEmpty { throw InvalidAstronautDataError.EmptyName } if age < 18 || age > 70 { throw InvalidAstronautDataError.InvalidAge } self.name = name self.age = age } |
Note that the initializer is now marked as throws
to let callers know to expect errors.
If an invalid input value is detected — either an empty string for the name, or an age outside the acceptable range — the initializer will now throw the appropriate error.
Try this out by instantiating a new astronaut:
let johnny = try? Astronaut(name: "Johnny Cosmoseed", age: 42) |
This is exactly how you handle any old throwing method or function. Throwing initializers behave just like throwing methods and functions. You can also propagate throwing initializer errors, and handle errors with a do
–catch
statement. Nothing new here.
To see the initializer throw an error, change johnny
‘s age to 17:
let johnny = try? Astronaut(name: "Johnny Cosmoseed", age: 17) // nil |
When you call a throwing initializer, you write the try
keyword — or the try?
or try!
variations — to identify that it can throw an error. In this case, you use try?
so the value returned in the error case is nil. Notice how the value of johnny
is nil
. Seventeen is too young for spaceflight, sadly. Better luck next year, Johnny!
To Fail or to Throw?
Initializing using a throwing initializer and try?
looks an awful lot like initializing with a failable initializer. So which should you use?
Consider using throwing throwing initializers. Failable initializers can only express a binary failure/success situation. By using throwing initializers you can not only indicate failure, but also indicate a reason by throwing specific errors. Another benefit is that calling code can propagate any errors thrown by an initializer.
Failable initializers are much simpler though, since you don’t need to define an error type and you can avoid all those extra try?
keywords.
Why does Swift even have failable initializers? Because the first version of Swift did not include throwing functions, so the language needed a way to manage initialization failures.
Where To Go From Here?
Wow — you’re not only halfway through getting humans to Mars, you’re now a Swift structure initialization guru! You can download the final playground for Part 1 here.
To learn all about Swift class initialization, carry on to Part 2 of this tutorial.
You can find more information about initialization in the initialization chapter of Apple’s The Swift Programming Language guide. If you have any questions or comments, please join the discussion in the forum below!
The post Swift Tutorial: Initialization In Depth, Part 1/2 appeared first on Ray Wenderlich.