|

for case and where: Filter While You Iterate

A sequence of values with only matching elements continuing through a Swift loop

A collection does not always contain values that need the same work. An upload history, for example, may contain queued files, completed uploads, and failures. A screen that lists successful transfers only needs the completed events and their file sizes. The other events still belong in the history, but not in that particular list.

A regular for loop can inspect every event and put an if case inside its body. That works. Yet the main action can become harder to see when the loop spends more space deciding whether an element qualifies than handling the element itself.

Swift lets us put that decision into the loop header. With for case, a pattern selects the elements that enter the body. With where, an additional condition can narrow the selection. This Molecule explores how the two parts work, what happens to nonmatching values, how optional and tuple patterns fit, and when an ordinary loop or switch communicates the intent better.

Match the case as you iterate

Suppose each upload event is one case of an enum. A completed upload carries both a filename and a byte count:

enum UploadEvent {
    case queued(String)
    case uploaded(String, bytes: Int)
    case failed(String)
}

let events: [UploadEvent] = [
    .queued("cover.png"),
    .uploaded("guide.pdf", bytes: 2400),
    .failed("notes.txt"),
    .uploaded("photo.jpg", bytes: 800)
]

for case let .uploaded(name, bytes) in events {
    print("\(name): \(bytes) bytes")
}
// guide.pdf: 2400 bytes
// photo.jpg: 800 bytes

The array contains four events in a fixed order. The loop’s .uploaded(name, bytes) pattern matches the second and fourth elements only. For each match, Swift binds the associated filename to name and the size to bytes. The output is guide.pdf: 2400 bytes followed by photo.jpg: 800 bytes. Neither .queued("cover.png") nor .failed("notes.txt") enters the body.

The let binds values for the current iteration; it does not change an event or remove anything from events. Think of for case as a regular sequence traversal with a pattern gate in front of the body. The order of the matching elements stays the order in which the sequence yields them.

The equivalent longer form is for event in events { if case let .uploaded(name, bytes) = event { ... } }. Both express the same selection. The shorter header is useful when the only reason to enter the body is that the pattern matches.

Try it: Add .uploaded("empty.dat", bytes: 0) to the array. It still prints a line. Matching .uploaded says nothing about whether its byte count is positive.

Add a domain rule with where

A matching enum case may not be enough. Suppose this view should display only completed uploads of at least 1,000 bytes. The additional rule belongs after where:

for case let .uploaded(name, bytes) in events where bytes >= 1000 {
    print("Large upload: \(name)")
}
// Large upload: guide.pdf

This loop uses the events array from the previous example. Both completed uploads match the enum pattern. The bound bytes is then tested against 1000: guide.pdf has 2400 bytes and prints; photo.jpg has 800 bytes and does not. Queued and failed events do not satisfy the pattern, so the body is skipped for them as well.

The order matters conceptually: first the pattern must match and provide bytes, then the Boolean condition can use that binding. The where here is part of a for statement. It is not a generic type constraint and does not modify the array.

A where condition is a filter, not a validation error. An upload with zero bytes is silently skipped in this view. If a zero-byte upload should produce an error message, a loop that handles only qualifying uploads is not enough.

Try it: Change bytes >= 1000 to bytes > 500. Both completed uploads now enter the body, in the same order.

Optional patterns can skip missing values

Not every collection needs an enum you define yourself. Imagine a sequence of optional HTTP status codes: some requests have not returned a status yet. We want only present codes in the success range.

let responses: [Int?] = [nil, 200, 404, nil, 201]

for case let status? in responses where (200..<300).contains(status) {
    print("Successful status: \(status)")
}
// Successful status: 200
// Successful status: 201

The pattern let status? matches a non-nil optional and binds the wrapped integer to status. The two nil elements fail the pattern and never enter the body. The 404 value matches the optional pattern but fails the range condition. The values 200 and 201 pass both checks and print in that order.

This example separates two questions: does a value exist? and does that value satisfy our rule? The ? in the pattern is optional-pattern syntax, not optional chaining. It is also not the same as forcing an optional with !.

You could use responses.compactMap { $0 } and then filter the resulting values. That is a reasonable alternative when you actually need a transformed collection or want to compose more operations. The loop is direct when the goal is to perform a side effect for each qualifying status, without producing another array.

Try it: Replace 201 with 301. The last print disappears because 301 is outside the half-open range 200..<300.

Tuple patterns are useful even without enum cases

The case keyword introduces a pattern, not necessarily an enum case. A tuple can be decomposed in the loop header, and where can describe a relationship between its parts.

let points = [(0, 0), (3, 3), (2, -2), (4, 1)]

for case let (x, y) in points where x != 0 && (x == y || x == -y) {
    print("Diagonal: (\(x), \(y))")
}
// Diagonal: (3, 3)
// Diagonal: (2, -2)

The pattern let (x, y) binds both coordinates. The condition rejects the origin (0, 0) because x != 0 is false, then accepts points whose coordinates are equal or opposite. That leaves (3, 3) and (2, -2); (4, 1) is neither diagonal and does not print.

A plain for (x, y) in points where ... is also valid Swift for this tuple example. That is usually the simpler spelling because every tuple has the same shape; case adds no selective enum matching here. The example is useful for understanding that for case uses the general pattern system, but it is not a recommendation to add case to every tuple loop.

Try it: Remove x != 0 &&. The origin now passes x == y and appears first in the output. The condition, not the pattern, defines which points count as interesting.

When filtering is the wrong abstraction

Selecting one kind of event is different from handling all meaningful event types. Consider a maintenance screen that must show large successful uploads and failed files that need a retry:

for event in events {
    switch event {
    case let .uploaded(name, bytes) where bytes >= 1000:
        print("Large upload: \(name)")
    case .failed(let name):
        print("Needs retry: \(name)")
    default:
        break
    }
}
// Large upload: guide.pdf
// Needs retry: notes.txt

Here the first switch case prints Large upload: guide.pdf, because its size is 2400; the 800-byte photo does not satisfy that case. The second case prints Needs retry: notes.txt. The queued event and small upload reach default, which intentionally does nothing. The code still traverses events in order.

This switch expresses two different actions in one place. A for case .uploaded loop could not also handle .failed in its body, because those values would never enter it. You could run two filtered loops, but then output would be grouped by type rather than preserve the original interleaving. If ordering across event kinds matters, that difference is observable.

There is another trade-off: for case silently ignores unmatched cases, while an exhaustive switch without a default can make a newly added enum case a compiler error until it is handled. The default in this example deliberately gives up that exhaustive-review benefit. Choose it only when ignoring other states is a conscious part of the design.

Try it: Replace default: break with explicit .queued and .uploaded branches as needed. Then add a new enum case and observe how a switch without default requires another decision.

The selection belongs where the intent is clearest

Use for case when each iteration should act on one recognizable shape, especially an enum case carrying associated data. Add where when a separate Boolean rule decides which matching values qualify. Both keep the selection close to the loop’s purpose, and neither changes the source collection.

Prefer a plain for when all elements need attention, a regular tuple binding is enough, or the condition is easier to understand inside the body. Prefer switch when several event types require different actions or exhaustive handling is part of correctness. Prefer filter or compactMap when the result should be a reusable collection rather than immediate work.

The key distinction is between selecting elements to process and deciding what every element means. for case is excellent at the first job. It should not quietly replace the second.

Sources
Next Postif case and guard case: Match One Shape Without a switch