3 lesser-known ways of using Swift enums

Swift Enum

An enumeration (enum) is a very powerful type in Swift. I use it a lot in my code. Today I'm going to introduce you to some techniques around Swift enumeration that you might not aware of.

1. Compound Cases with value bindings #

A compound case is when you want to match several cases together. You can do that by writing comma-separated patterns after case.

The following is an example from Swift Documentation. We combine each character into two compound cases vowel and consonant.

let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("\(someCharacter) is a consonant")
default:
print("\(someCharacter) is not a vowel or a consonant")
}

What you might not aware of is, compound cases also work with enumerations with associated values. All of the compound case patterns have to include the same set of value bindings, and each binding has to get a value of the same type from all of the patterns in the compound case.

enum Job {
case developer(exp: Int)
case designer(exp: Int)
case pm(exp: Int)
}

let job = Job.developer(exp: 5)

switch job {
case .developer(let exp), .designer(let exp), .pm(let exp):
print("You have \(exp) year(s) of experience")
}

The above example will print out the number of years regardless of the job position.

Different number of associated values #

The same set of value bindings doesn't mean the same number of associated values. You can compound cases with different numbers of associated values as long as you bind the same number of values.

Let's see this in action. I have created a sample enumeration called Foo. It contains cases with a different number of associated values and data types.

enum Foo {
case a(Int)
case b(Int, Int)
case c(Int, String, Int)
case d(String)
}
let test = Foo.c(1, "c", 3)
switch test {
case .a(let i), .b(_, let i), .c(let i, _, _):
print(i)
default:
break
}

Output:

1

As you can see, we can compound cases with one, two, and three associated values together because we bind them with the same number of values (one) and the same data type (Int).

Order #

The order of associated values doesn't matter. Just make sure the same binding has the same data type.

In the following case, we bind two values i and j. You can see that i doesn't need to be the first binding.

let test = Foo.c(1, "c", 3)
switch test {
case let .b(i, j), let .c(j, _, i):
print("(\(i), \(j))")
default:
break
}

Output:

(3, 1)

Caveat #

There is one caveat that you should know about compound cases. If you use where clause to check for additional conditions, make sure you apply that to all the cases in compound cases.

let test = Foo.a(1)
switch test {
case .a(let i) where i == 5,
.b(_, let i) where i == 5:
print("Five(\(i))")
default:
print("Not five")
}

Output:

Not five

The above will print out "Not five" since 1 is not equals 5 (obviously).

If you apply where only at the end of the case, it will only apply to the last pattern.

let test = Foo.a(1)
switch test {
case .a(let i),
.b(_, let i) where i == 5: // 1
print("Five(\(i))")
default:
print("Not five")
}

<1> Where clause will only apply to .b.

Output:

Five(1)

The above will print out "Five(1)" since the where clause only applied to .b.

2. If case let / Guard case let #

If you care only specific enumeration case, you can use if let and guard let form.

let test = Foo.a(1)

if case .a = test {
// Action for .a case
}

guard case .a = test else {
return
}

You can use value binding just like you do with switch case.

if case .a(let i) = test {
// Action for .a case
}

if case let .a(i) = test {
// Action for .a case
}

3. Optional #

If an associated value is optional, you can also pattern matching an optional value.

It is easier to understand this in action. Before we do that, let me define a new enumeration for this. It looks similar to Foo, but this time all associated values are optional.

enum Bar {
case a(Int?)
case b(Int?, Int?)
case c(Int?, String?, Int?)
case d(String?)
}

If you want to extract the first value from .a, you can do it like this.

let test: Bar = Bar.a(1)
switch test {
case .a(let i):
print(i)
default:
break
}

The above case would match .a regardless of its value. So, both Bar.a(1) and Bar.a(nil) will matched.

.some(T) #

If you want to match case .a only when the associated value is non-nil value, you can do it like this.

let test: Bar = Bar.a(1)
switch test {
case .a(let .some(i)):
print(i)
default:
break
}

Since optional is just another enum. We use case .some to match a non-nil value here.

enum Optional<Wrapped> {
case none
case some(Wrapped)
}

.none #

As you might know by now, we can match a nil value with .none.

The following switch will match .a case with a nil value.

let test: Bar = Bar.a(nil)
switch test {
case .a(.none):
print("nil value")
default:
break
}

Mixed and matched #

You can use this in a case with multiple associated values to get a condition that you want.

let test: Bar = Bar.c(1, "Cat", nil)
switch test {
case .c(let i, .some, .none):
print(i)
default:
break
}

The above switch case would match a .c case and retrieve the first value regardless of its value, but only if it has a non-nil second string value and the third value of nil.

Conclusion #

I always forget about these three features (there are more, but I can't think of them right now), that's why I sum it up here. If you find this article useful, please share it with your friends and follow me for more.


Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Tweet Share

If you enjoy this article, you can subscribe to the weekly newsletter.

Every Friday, you’ll get a quick recap of all articles and tips posted on this site — entirely for free.

← Home