Swift’s switch statement is a lot more powerful than initially expected. In many other languages, the switch is a glorified simplification of an if statement over every possible value contained in an enumeration. However, in Swift, there is a great deal more control and flexibility. At this moment in time, the documentation around Swift’s switch Swift’s Control Flow is somewhat lacking, but nevertheless, we’ll try and build up your knowledge from the ground up.

Basics

It’s hard to talk about the switch statement without talking about things like enums, tuples, and intervals.

Just a refresher, we can create an enum like this:

enum Basic {
    case one, two,three, four
}

Or alternatively:

 enum Basic {
    case one
    case two
    case three
    case four
 }

We can then use the enum like this:

let basicOne = Basic.one

If we wanted to perform logic depending on what case we have in an enum, we can do so without the switch, but it can get a bit clunky:

if basicOne == .one {
    print("I am case one")
} else if basicOne == .two || basicOne == .three {
    print("I am case two or three")
} else if basicOne == .four {
    print("I am case four")
}

But we could also write the same like so with a switch:

let basicOne = Basic.one
switch basicOne {
case .one:
    print("I am case one")
case .two, .three: // Catch multiple cases
    print("I am case two or three")
default:
    print("I am case four")
}

No Fallthrough

Swift’s switch is said not to fallthrough. This means that if we hit the switch’s case .one: in the previous example, the other cases won’t run. If we want to override the behavior and make things fallthrough to the next case, we can do so like this:

let basicOne = Basic.one
switch basicOne {
case .one:
    print("I am case one")
    fallthrough
case .two, .three: // Catch multiple cases
    print("I am case two or three")
    fallthrough
default:
    print("I am case four")
}

We’ll actually see this in the console:

I am case one
I am case two or three
I am case four

Must be exhaustive

switch statements must be exhaustive. That means, whatever we’re switching over, we need to make sure that we handle every possible case, or we’ll get a runtime error. The fastest way to make a switch statement exhaustive, is to add a default statement which will catch everything that has not already been handled. However, many prefer to omit the default statement, as it forces one to implement every possible case we could get. This could be particularly important when switching over an enum that has a new case, because we might forget to implement that case otherwise.

Associated Values / Value Binding

As we can store values in enumeration cases, called associated values, we get also get access to those values in if let or guard let statements like so:

enum BasicAssociatedValue {
    case one(String)
    case two(String, Int)
}
let basicAssociatedValue = BasicAssociatedValue.one("Hello world")

if case let .one(someValue) = basicAssociatedValue {
    print("someValue is \(someValue)")
}

let basicAssociatedValueTuple = BasicAssociatedValue.two("Bob", 42)
if case let .two(text, number) = basicAssociatedValueTuple {
    print("text is: \(text) and number is: \(number)")
}

We can also get access to values in an enum with an associated value within a switch statement like so:

switch basicAssociatedValue {
case let .one(text): // or case .one(let text):
    print("text is: \(text)")
case let .two(text, number): // or case .two(let text, let number):
    print("text is: \(text) number: \(number)")
}

We can also ignore one or both values of an associated enum using the _ like so:

switch basicAssociatedValue {
case let .one(_): // or case .one
    print("text is some unknown value")
case let .two(_, number):
    print("number: \(number)")
}

Intervals

We can write switch statements to trigger cases based on whether or not they match an interval. (eg, 0..2, ..<5, etc…)

let number = 2

switch number {
case 0...5:
    print("number in range of between 0...5")
case 6:
    print("number is exactly 6")
default:
    print("number neither in 0...5, or exactly 6")
}

Tuples

We can also use the switch over a tuple.

let someTuple: (String, Int) = ("Hello", 42)

switch someTuple {
case (text, number):
    print("text  is: \(text) number is:\(number)")
default:
    print("Needed to make compiler happy")
}

Where clause

Case statements can also include a where clause. In order for a case to be matched with a where clause, both the initial case and the where clause will need to matched.

switch number {
case 0...5 where number % 2 == 0:
    print("number is 0...5 and I am also even")
case 0...5 where { number % 2 == 0 && number == 2 }(): // Silly example, however we can use a closure or function to perform additional logic
    print("number is 0...5 and is exactly number 2 as well")
case 0...5:
    print("number is 0...5 and I am also odd")
default:
    print("number is not in 0...5")
}

If we have multiple cases that will match a value, the first one that matches will take precedence.

Unfortunately, we are unable to specify multiple where clauses with a , as we can with generic constraints at the time of writing, so this would not compile:

// ⛔️ where clause that does not compile ⛔️
switch number {
case 0...5 where number % 2 == 0:
    print("number is 0...5 and I am also even")
case 0...5 where number % 2 == 0, number == 2: // Won't compile ⛔️
    print("number is 0...5 and is exactly number 2 as well")
case 0...5:
    print("number is 0...5 and I am also odd")
default:
    print("number is not in 0...5")
}

Switching on nested types

Switching on nested types is an often overlooked feature of the switch statement. Particularly when one is exposed to things like The Composable Architecture, we often find ourselves needing to nest switch statement within switch statements.

enum Foo {
    case a(Bar)
    case b(Planet)
}

struct Bar {
    let text: String
    let number: Int
}

enum Planet {
    case Earth(Bar)
}

let foo = Foo.a(Bar(text: "32", number: 42))

switch foo {
case let .a(bar):
    print("bar is: \(bar.number) number:\(bar.number)")
case let .b(planet):
    switch planet {
    case let .Earth(bar):
        print("Earth's bar is: \(bar.number) number:\(bar.number)")
    }
}

However, we actually don’t need to write a nested switch statement as was just shown. We can do something like this a lot more elegantly like so:

switch foo {
case let .a(bar):
    print("bar is: \(bar.number) number:\(bar.number)")
case let .b(.Earth(bar)): // Multi level matching
    print("bar is: \(bar.number) number:\(bar.number)")
}

This might seem like a particularly simple language feature, however it becomes incredibly powerful whenever we’re dealing with lots and lots of switch statements. It allows us to flatten out and un-indent our code dramatically, improving readability, which is an extremely significant metric when we think of the quality of code.

The ~= operator

Whenever the built in pattern matching of case statements are insufficient, we can implement our own custom pattern matching using the ~= operator.

struct Television {
    let age: Int
    let sizeInInches: Int
    let resolution: (Int, Int)
}

enum TelevisionUseCase {
    case sportsEvent
}

extension Television {
    static func ~= (useCase: TelevisionUseCase, value: Television) -> Bool {
        if useCase == .sportsEvent,
           value.age < 5,
           value.resolution.0 >= 1920,
           value.resolution.1 >= 1080,
           value.sizeInInches >= 42 {
            return true
        }
        return false
    }
}

let television = Television(age: 2, sizeInInches: 50, resolution: (1920, 1080))
switch television {
case .sportsEvent:
    print("Television is good enough for sports")
default:
    print("Television is not good enough for sports")
}

Here we’ve extended our Television to be able to switch on TelevisionUseCase. In our ~=, we have business logic to handle switching on a television using a TelevisionUseCase. In this way, we can write our own pattern matching to our hearts content.

There, your now an expert on the switch statement 😛