Before we go on, here's some tips for users of other programming languages this year:

  • Rust also supports optional types. Unlike Swift, Rust does not provide as much syntactic sugar but the concepts are largely the same.
  • Java supports an Optional type since version 8. However, much of the standard library only returns null rather than Optional, so in many cases you would need to use Optional.ofNullable. Scala and Kotlin make more comprehensive use of Optional
  • C# supports a similar feature through nullable-reference types
  • C++ supports std::optional. Like Java, many built-in functions and the standard library simply return null. You may need to handle null in a different way, though you could still use std::optional in the design of your own code

Because optional values may have been added to your language's standard library later in its lifecycle, you may need to handle null references in the traditional manner. For example, you might need to use try/catch to handle a lot of potential errors in Java and C#.

In Swift, optionals allow variables to have either a value or nil (no value). This is useful when:

  • an expected value is missing
  • a value cannot be cast to another data type (i.e. String to Int)

Example:

var name: String? = "John"

In the above example, name is an optional string, meaning it may contain a String or be nil. Since the value is optional, Swift safely wraps the data inside an Optional container.

Technically speaking, the value is Optional.some("John") — users of Rust and other languages with functional programming facilities may find this familiar.

var name: String? = "John"
name = nil

Optional variables can be set to nil, as in line 2 above. Note: this is similar to setting a variable to None in Python — except that Swift guarantees safer handling of these kinds of values by actually wrapping it in a value called Optional.none.

Finally, Swift may give you an optional value when casting between data types. For example, let's create an integer (Int) and a number with a decimal point (Double):

let age: Int? = Int("17")
let highestScore: Double? = Double("GAME OVER")

Use ? after the type hint to define an optional type:

let age: Int? = 25  // Can be 25 or nil, equivalent to Optional.some(25)
let username: String? = nil  // No value assigned, equivalent to Optional.none

Types inside types can also be optional. For example, a list of optional integers is [Int?]. Furthermore, an optional list of optional integers is [Int?]?.

To safely use an optional, it must be unwrapped. This means that we carefully check if the value exists or not; if it does, we can use it. If there is no value (Optional.none), the program safely handles the absence of the value and moves on. The unwrapped value is a concrete value.

In Python, it is possible to write code that does not handle when a value is None, causing the program to crash when you run it. Swift detects when an optional value hasn't been unwrapped and refuses to compile the code, forcing you to write safer code that is easier to test.

Use if let to check if an optional contains a value:

let city: String? = "Te Whanganui-a-Tara"

if let unwrappedCity = city {
    print("City: \(unwrappedCity)")
} else {
    print("No city supplied")
}

The unwrapped value is only usable inside the brackets. This means that the scope of that unwrapped value is limited to that part of the program only.

If you were to unwrap a value and then need to cast it to a different type, that cast would need to occur inside the brackets.

let userInput: String? = readLine()

if let ageString = userInput {
    if let age = Int(ageString) {  // Int() returns an Int?, so we unwrap it here.
        print("In ten years, you will be \(age) years old!")
    }
}

  1. Use print to ask for the user's age (Enter your age:)
  2. Print a blank line using print() again
  3. Use readLine() to wait for the user to type something. (This is similar to input() in Python).
  4. Use if let to unwrap the value provided by readLine() into a new constant called ageString
    • If the string was able to be created, move on to step 6 inside this if block
    • If the string was not able to be created (unlikely; see below)

readLine returns an optional String in case something unusual happens with the program that causes the function to return before any text is given; this is exceedingly unlikely to happen but you still need to do safe unwrapping, just in case!

  1. Try converting the user's input in ageString to a new integer using Int()
  2. Use if let to safely unwrap the value provided by Int() into a new constant called age
    • If a number was able to be created, print a message stating the user's age
    • If a number was not able to be created, print a message complaining that the input is invalid

If you know an optional contains a value, use ! when accessing the value to force unwrap it:

let language: String? = "Swift"
print(language!)  // Works if language is not nil, crashes otherwise.

Warning: Force unwrapping an optional that contains nil causes your program to crash.

  1. Create a copy of your code from the previous task
  2. Instead of using if let, just force unwrap the value returned by readLine and Int
    • Remove the printed messages informing of an error — forced unwrapping means we assume no error will occur!
  3. Run the program and try typing something — can you get the program to crash?

As you will have seen in the notes on scope, unwrapping values successive times requires staying within the brackets. This can become very unwieldy if you have multiple unwraps indented like so:

if let b = a {
    if let c = Int(b) {
        if let d = Bool(c) {
            if let // ... etc.
        }
    }
}

This construct is called the pyramid of doom.

Instead of nesting these if let statements multiple times, you can join them into a single conditional statement. Join the let statements together using commas.

if let b = a, let c = Int(b), let d = Bool(c) {
    // Here, you can use b, c, and d safely
}

A very useful construct is guard let. It does largely the same job as if let. However, it allows unwrapped values to have the same scope as they have. To illustrate this, let's look at an if let with limited scope:

if let b = a {
    // b can only be used here. It has limited scope.
}

// b is NOT usable here. b's scope is limited to inside the if let.

However, with guard let, b can be used at the same level of 'indentation' (so to speak) as the statement itself. To do this, guard let contains an else block that specifies what happens if the value could not be unwrapped.

guard let b = a else {
    print("Unable to unwrap the specified value. Exiting...")
    exit(1)  // This completely closes the program with exit code 1.
}

print(b)  // b is usable here and anywhere else in the program (beyond this point).

Using guard let ... else is very good for avoiding the pyramid of doom we saw above. It also requires you to handle errors explicitly, leading to safer code that is easier to test.

guard let's else block must always exit the current scope. This means that:

  • in top level code (such as we are writing now), the program must exit. Use exit(1), signifying that the program has exited with an error.
  • in a for or while loop, you can use break or continue:
    • break completely ends the loop, regardless of whether the condition has been met
    • continue stops the current iteration of the loop but continues with the next; this is useful for allowing for loops to completely finish.
    • Note: these also apply to .forEach

  • in functions:
    • a function that does not return a value, you can use return to prematurely exit the function
    • in functions that return a non-optional value, you can return a constant
    • in functions that return an optional value, you can return a constant or nil
    • map must always return a value. However, if you wish to return nil, you can use compactMap instead (this results in a new collection with fewer items than the original — or even none at all!)

You work for an airline and need to create a simple Swift program that lets users book a seat and select a meal preference. Since users may not always enter valid data, you must safely unwrap all user input.

  1. Ask the user for their seat number (as an integer).
    • Use readLine() to accept input
    • Convert it to an Int? using Int()
      • If valid, print "You have booked seat number X" (where X is their seat number).
      • If invalid, print "Invalid seat number. Please enter a number."
  2. Ask the user for their meal preference.
    • Provide three options: "1. Vegetarian", "2. Standard", "3. Kosher"
    • Read their input as an Int?
      • If valid, print "You have selected the X meal."
      • If invalid, print "Invalid meal choice."
  3. Ask if they want extra legroom (yes/no)
    • Read their response as a String?
    • Convert the input to lowercase using .lowercased() (i.e. myString.lowercased())
      • If they type "yes", print "Extra legroom added."
      • If they type "no", print "No extra legroom selected."
    • If the input is invalid (e.g., empty or unrecognized), print "Invalid choice."
  4. Ask for the passenger’s weight (to estimate fuel load, as a Double)
    • Convert the input to a Double?
      • If valid, print "Passenger weight recorded: X kg" (where X is their weight)
      • If invalid, print "Invalid weight entry. Please enter a number."

Enter your seat number: 12  
You have booked seat number 12  

Select your meal preference:  
1. Vegetarian  
2. Standard  
3. Kosher  
Enter the number: 2  
You have selected the Standard meal.  

Do you want extra legroom? (yes/no): yes  
Extra legroom added.  

Enter your weight (kg): 75.5  
Passenger weight recorded: 75.5 kg  

Enter your seat number: A12  
Invalid seat number. Please enter a number.  

Select your meal preference:  
1. Vegetarian  
2. Standard  
3. Kosher  
Enter the number: 5  
Invalid meal choice.  

Do you want extra legroom? (yes/no): maybe  
Invalid choice.  

Enter your weight (kg): seventy  
Invalid weight entry. Please enter a number.

  1. After completing the task above, try creating a while loop to keep asking for input to ensure user enters valid data
  2. Try using advanced unwrap techniques such as multiple lets in a single condition, guard let, or (ideally) both