How to Use "where" in Swift

August 2024 · 10 minute read
Back to blog
By Aasif Khan | Last Updated on September 26th, 2023 3:41 pm | 4-min read

You use the where keyword to filter things in Swift. Loop over all items where x = true, for example. It’s simple and powerful! In this mobile development tutorial, we’ll discuss the various scenarios in which you can use where in Swift.

Here’s what we’ll get into:

Table of Contents

You use where in Swift to filter things, kinda like a conditional. In various places throughout Swift, the “where” clause provides a constraint and a clear indicator of the data or types you want to work with.

What’s so special about where – as you’ll soon see – is its flexible syntax. For example, for item in array where item.x == true will get you all array items for which a condition x == true returns true. It’s so concise, and so powerful.

What’s intriguing about the where “keyword” in Swift, is that you can use it in seemingly unrelated places. You can filter a construct like a for in loop or switch with “where”, but you can also use it to constrain extensions or generics to a certain type. Moreover, you see “where” also in common higher-order functions like first(where:).

Before we discuss how “where” works in these different scenarios, it’s important that you understand that where in Swift always filters something. “Get me all of X where X.x = y”. When you look at it like this, reading Swift code that uses where becomes incredibly intuitive – and that’s where‘s superpower.

Let’s dive in!

Author’s Note: Before I mostly worked on iOS (and Android) apps, I did a lot of web development with PHP and MySQL. You use SQL to get data from databases, and the SQL language includes a construct called a “WHERE clause”. Example: SELECT * FROM fruits WHERE type = ‘Apple’. When you’re reading such a query, it may help to read it right-to-left: Where type is Apple, select (all) fruits. The resulting data is a subset of the whole; a filtered set, which may help you reason better about your code.

The first use case for “where” we’ll discuss is in the for in loop. With the for in loop you can loop over arrays, ranges, etcetera. You can filter the items to loop over with where.

Here’s an example:

let primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]

for prime in primes where prime < 10 { print(prime) } // Output: 2 3 5 7 In the above code we’ve got a bunch of prime numbers. With the for value in collection where condition syntax, we’re filtering integers from primes for which the condition prime < 10 is true. What’s interesting is that we can use the prime constant, as defined in for prime in also in the where clause itself. This prime constant refers to the current item in the primes array. You typically use this inside the body of the loop, between { and }. When you compare the above code that uses where, with the code below, it becomes clear that using “where” in Swift is syntactic sugar. for prime in primes { if prime < 10 { print(prime) } } // Output: 2 3 5 7 Both these Swift code examples have the same result, but the one with where is more concise and easier to read. The where clause “obscures” the uglier syntax, hence “syntactic sugar”. It’s also good to know that using where in a for in loop has viable alternatives. Check this out: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29].filter { $0 < 10 }.map { print($0) } // Output: 2 3 5 7 The above code uses the filter(_:) and map(_:) higher-order functions to filter and transform the array of numbers. Again, the result is the same but the implementation is different. It’s up to you to choose which alternative is best in your own code. You can use any kind of expression for a where clause, including conditionals like > greater than, type checking with is, and functions like isMultiple(of:).

You can also use where with switch. You use the switch statement in Swift to match a value with one of several patterns. You can use where to define an additional condition for these patterns.

Let’s take a look at an example:

enum Item {
case weapon(Int, Int)
case potion(Int)
case armor(Int, Int, Double)
}

let item = Item.weapon(75, 9)

switch item {
case .weapon(let hitPoints, _) where hitPoints > 50:
player.slowAttack(hitPoints) // Heavy weapon attack
case .weapon(let hitPoints, let weight):
player.attack(hitPoints, speed: weight * 10)
case .potion(let health):
player.health += health
case .armor(let damage, let weight, let condition):
player.damageThreshold = damage * condition
}

In the above code we’ve created a scenario for an imaginary Role Playing Game (RPG). You’ve got an item in your inventory, which can be a weapon, a potion or an armor. The Item enum uses associated values to define stats like hitpoints, damage and health.

In the switch block, we’re determining what happens for each of the item types. A weapon is used to attack, for example, and a potion is used to increase the health of the player. Each of these scenarios and actions are coded within the switch and case statements.

This case is special, though:

case .weapon(let hitPoints, _) where hitPoints > 50:

It defines an additional condition which must be true for the case to match. Consider that you have a .weapon assigned to item. One of 2 things is going to happen:

See how that works? It’s like combining a case pattern match with an additional condition. It lets you get super clear about what kind of pattern you want to match, under which conditions. And just as before, you can use any kind of expression with where. Neat!

Next to for in and switch, you can also use where with guard let, while, if let and do-catch, in a similar fashion. Keep in mind that when where filters, it’ll also exclude something. In case of catch, for example, you’ll want to ensure you catch some or all errors.

In the previous sections, we’ve looked at how you can use where to apply a filter in the context of values, like arrays. You can, however, also use where to apply filters in the context of types. Let’s dive in!

Check out the following extension in Swift:

extension Array where Element == String {
func reverseAll() -> [String] {
return self.map { String($0.reversed()) }
}
}

let names = [“Arthur”, “Ford”, “Zaphod”, “Trillian”]
print(names.reverseAll())
// Output: [“ruhtrA”, “droF”, “dohpaZ”, “naillirT”]

In the above code, we’ve defined an extension for the Array type. This adds a function reverseAll() to Swift’s pre-existing type for arrays. The function itself will call reversed() on every array item.

Logically, the reversed() function only works on arrays of strings. That’s why we’ve constrained the extension with where Element == String. You can read this as: Only add this extension’s functions for array’s whose elements are of type String.

The code makes use of the Element generic placeholder, which is a “stand in” for the actual type of an array. That’s a concept that belongs to generics, not where clauses, but it’s important to point out here.

In the above code, we’re directly comparing types with the equals operator ==. This works well for concrete types like String, but what if you want to constrain an extension to, say, a protocol?

Check this out:

extension Sequence where Element: Hashable {

func unique() -> Set {
var uniques = Set()
for item in self {
uniques.insert(item)
}
return uniques
}
}

let numbers = [1, 1, 3, 9, 22, 3, 4, 5, 22, 9]
print(numbers.unique())

What’s going on here?

First off, the unique() function takes a crude approach to removing duplicates from any kind of sequence, like an array of numbers. It does so by relying on a characteristic of the Set type, namely, that any value in a Set must be unique.

Comparing if two elements in a Set are equal happens by comparing their hashes. Any type that has a hash – it is-hashable – conforms to the Hashable protocol. The unique() function simply adds all items to a Set, and the insert(_:) won’t add an already existing item to the set if its hash is already present.

Based on the requirement that the items in the sequence must be hashable, we can define a constraint for the extension of where Element: Hashable. This means that the Element of the sequence, such as String for an array of strings, must conform to the Hashable protocol. As such, we can only use the unique() function on sequences, collections, arrays, etc. whose items conform to Hashable. Neat!

Why go through all this trouble to remove some duplicates from an array of numbers? Well, these examples are trivial of course. They’re merely here to explain the principle, nothing more. In real life though, imagine you’ve got a bunch of custom objects like User or Tweet. You need to remove duplicates in a few places in your code, which is why you “abstract away” the deduplicating function into an extension constrained to specific but flexible types.

So far we’ve looked at using where to filter data and types, but there’s a third spot where you’ll encounter where clauses: in higher-order functions. In this usage scenario, where isn’t syntax, but a convention for function names and arguments.

Take a look at the following example:

let names = [“Arthur”, “Ford”, “Zaphod”, “Trillian”]

if let name = names.first(where: { $0.contains(“a”) }) {
print(name)
}
// Output: Zaphod

In the above code, we’re trying to find a string in an array of strings that contains an “a”. We’re doing so with the first(where:) function. This is a higher-order function, which means it’ll take a closure as an argument for the function itself.

We’re applying the closure { $0.contains(“a”) } to every item in the names array. As soon as the closure returns true for a string in the array, that string is returned. We’re wrapping this in an optional binding block with if let, because first(where:) naturally returns an optional value.

There’s more of them, by the way:

What is where in these functions? In this code, “where” is an argument label of the first(where:) function. Unlike before, with where Element == Type. This “where” convention is followed throughout Swift, so you know you’re dealing with a function that filters with a where clause. Awesome!

OK, before you go, check this out:

let names = [“Arthur”, “Ford”, “Zaphod”, “Trillian”]

for name in names where name.contains(“a”) {
print(name)
break
}
// Output: Zaphod

We’ve come full circle! The above code has the same result as the one with first(where:), and they both use “where”, but they’ve got completely different implementations. Goes to show that coding Swift is often more about finding the right implementation, than about finding the right result.


Related Articles

App Builder

ncG1vNJzZmivp6x7orzPsqeinV6YvK570rCgn6xdrLWmvsRmn6ivXam8