What are Optionals in Swift?
Published on: August 12, 2024In an earlier article, I explained how variables are defined in Swift using let and var. Both constants (let
) and variables (var
) in Swift always have a type; it's what makes Swift a strongly typed language.
For example, we could define a String
variable like this:
// the compiler will know myString is a String
var myString = "Hello, world"
// we're explicitly telling the compiler that myString2 is a String
var myString2: String = "Hello, world"
This way of defining variables makes a lot of sense when it's possible to immediately assign a value to our variable.
However, sometimes you'll write code where it's not possible to assign a value to your variable immediately. Or you're working with functions that may or may not be able to return a valid value.
In Swift, we call values that can distiguish betwene having a value and not having a value an Optional
. Before we dig too deeply into how we work with optionals, let's explore the difference between "no value" and "default" value so that we understand exactly why optionals exist in the first place.
If you prefer to learn through video instead of text, check out this video on my YouTube channel
The difference between a default value and no value
In programming, working with a concept called null
or as Swift calls it nil
will generally mean that a variable or a function's return value can be "nothing". There's a lot of technical baggage surrounding the terminology but in order to establish a good working knowledge, we won't dig into that too deeply.
The important thing to understand here is that defining an empty String
like this: var myString = ""
defines a String
with a default value. The value is empty but the var myString
is holding on to some data that will represent an empty String
. Often this is a perfectly fine choice.
Now let's consider a different example where a default value would be a lot harder to define.
var theUser: User = ???
Our User
object can't be created without input from other sources. And this input might not be present at that time that we define our variable. We'll need a way to define this var theUser
with no data rather than a default value.
A real world analogy you might think of is the following. When you sit down at a cafe for some drinks, you will initially have no glasses or cups on your table. As a result, your waiter will know that you haven't been served anything at all so they'll know to go over and hand you a menu, introduce themselves and see whether they can take any orders. Once you've been served you might have some empty glasses on your table. The waiter will now know to ask to refill or take a different order.
This is a demonstration of how no value (no glass on the table) and an empty value (empty glasses on the table) can have significant differences in meaning and they can't always be used interchangeably.
In Swift, we express the ability of a property having no value rather than a default value by defining an optional User
object:
var theUser: User?
The ?
after our User
tells the compiler that var theUser
will either contain a value of type User
or it will hold nothing at all (we call this nil
).
It's nice to know that the ?
is a more convenient to write the following:
var theUser: Optional<User>
While the two ways of defining theUser
do the same thing, it's best practice to write var theUser: User?
. It's easier to read and faster to write.
Note that all types in Swift can be written as an optional. For example, if you're defining a String
that might need to be initialized as "no value" you could write: var theString: String?
.
The main difference between "no value" and "default value" is often whether there's any semantic meaning to pointing at nothing or pointing to a default value. For example, an optional Bool
(Bool?
) almost never makes sense; in most scenarios you will be able to pick a sensible default value that's safe to use. In other cases, something being empty or missing could indicate that input from the user is required, or that you need to fetch data from an external source and it's not possible or reasonable to provide a default value.
Now that you know how to write optional properties, let's see how optionals are used in Swift.
Using optionals in your code
Once you've defined an optional value in Swift, it's important that we handle the possibility of a value being nil
as well as the value being non-nil
. Swift is pretty strict about this so optionals aren't used in the same way as you would use normal variables or constants.
For example, if we consider the theUser
variable from earlier, we can't read the name from this property like this:
var theUser: User?
// Value of optional type 'User?' must be unwrapped to refer to member 'name' of wrapped base type 'User'
print(theUser.name)
The Swift compiler will tell us that we need to "unwrap" value of optional type User?
in order to access its member name
. This is the compiler's way of telling us that theUser
may or may not be nil
so we need to handle both scenarios.
Let's take a look at severals ways in which we can "unwrap" our optional.
Unwrapping with if let
If we're writing code where we want to only execute a part of our script or function in case the value isn't nil
, we can use something called an if let
unwrap. Here's what that looks like:
var theUser: User?
// somewhere else in the code...
if let userValue = theUser {
print(userValue.name)
} else {
print("the user is nil")
}
This if let
attempts to read theUser
and we assign it to a constant. This constant is then made available inside of the if's body where we know that userValue
is of type User
. Outside of our if body we won't be able to access userValue
; it's only made available inside of the if. As needed, we can provide an else
to handle scenarios where theUser
is nil
.
Note that the code above could be simplified a bit. Swift allows us to use something called a shadow variable (variable of the same name) for theUser
which would change the if let
as follows:
var theUser: User?
// somewhere else in the code...
if let theUser {
print(theUser.name)
} else {
print("the user is nil")
}
Note that theUser
inside of the if body is not the same variable as theUser
outside of the if body; it's a different property with the same name. For that reason, theUser
inside of the if body is of type User
and outside of the if body it's User?
. This feature of Swift is nice when you're familiar with optionals but I find that sometimes it's better to provide a different name so that it's clear when you're using your unwrapped property or when you're using your optional property.
Unwrapping optionals with guard let
While if let
is great for usage inside of code where it doesn't matter that much whether a value is or isn't nil
, you sometimes want to make sure that a value isn't nil
at the start of a function. With if let
this would generally mean that you write an if let
at the start of your function and then write the whole function body inside of your if let
:
func performWork() {
if let unwrappedUser = theUser {
// do the work
}
}
This works but it can lead to a lot of nested code. For scenarios where you only wish to proceed in your function if a value is not nil
, you can use guard let
instead:
func performWork() {
guard let unwrappedUser = theUser else {
return
}
// do the work
// unwrappedUser is available to all code that comes after the guard
}
A guard
allows us to ensure that our user has a value and that the unwrapped value is available to all code that comes after the guard
. When we're using a guard
we must provide an else
clause that exits the current scope. Usually this means that we put a return
there in order to bail out of the function early.
Unwrapping multiple properties
Both if let
and guard let
allow us to unwrap multiple properties at once. This is done using a comma separated list:
if let unwrappedUser = theUser, let file = getFile() {
// we have access to `unwrappedUser` and `file`
}
The syntax for guard let
is the same but requires the else
:
guard let unwrappedUser = theUser, let file = getFile() else {
return
}
// we have access to `unwrappedUser` and `file`
Note that writing your code like this will require all unwraps to succeed. If either our user or file would be nil
in the example above, the if body wouldn't be executed and our guard
would enter its else
condition.
Reading through optional chaining
When you're working with an optional and you'd like to get access to a property that's defined on your object, you could write an if let
and then access the property you're interested in. You saw this earlier with User
and its name
property:
if let theUser {
print(theUser.name)
}
If we know that we're only interested in the name
property we can use a technique called optional chaining to immediately access the name
property and assign that to the property we're writing the if let
for instead.
Here's what that looks like
if let userName = theUser?.name {
print(userName)
}
This is very convenient when we're in a situation where we really only care about a single property. If either theUser
is nil
or (if name
is optional) name
is nil
the if body won't be executed.
We can use this technique to access larger chains of optionals, for example:
if let department = theUser?.department?.name {
}
Both theUser
and department
are optionals and we can write a chain of access using ?
after each optional property. Once any of the properties in the chain is found to be nil
the chain ends and the result is nil
.
For example, if we just assign the chain from above to a property that property is a String?
// department is String?
let department = theUser?.department?.name
The name
on the department property doesn't have to be a String?
but because we're using optional chaining we'll get a nil
value if either theUser
or department
is nil
.
This leads me to one last method that I'd recommend for working with and that's using the nil coalescing operator.
Unwrapping optionals using nil coalescing
For any optional in Swift, we can provide a default value inline of where we access it. For example:
let username: String?
let displayName = username ?? ""
The ??
operator in the example above is called the nil coalescing operator and we can use it to provide a default value that's used in case the value we're trying to access is nil
.
This is particularly useful when you need to provide values to render in a user interface for example.
You can also use this technique in combination with optional chaining:
// department is String
let department = theUser?.department?.name ?? "No department"
Now, let's take a look at one last method to unwrapping that I'm only including for completeness; this approach should only be used as a last resort in my opinion.
Force unwrapping optionals
If you're 100% absolutely sure that an optional value that you're about to access can never be nil
, you can force unwrap the optional when accessing it:
print(theUser!.name)
By writing an !
after my optional variable I'm telling the compiler to treat that property as non-optional. This means that I can easily interact with the property without writing an if let
, guard let
, without optional chaining or without using nil coaslescing. The major downside here is that if my assumptions are wrong and the value is nil
after all my program will crash.
For that reason it's almost always preferred to use one of the four safe approaches to unwrapping your optionals instead.