Use a Swift task group to collect competing calculations in completion order, skip nil, and cancel the remaining children when a value arrives. This Mac command-line example finds a usable rectangle layout for synthetic tiles and explains when the result can actually return.
Define what counts as the first useful result
Suppose a small desktop layout tool needs to arrange 221 synthetic tiles into a rectangle. Several local calculations search different possible row counts. Any nontrivial factor is acceptable: 13 rows and 17 columns works, as does 17 rows and 13 columns. The reader’s task is to accept whichever valid calculation completes first.
This is a completion-order contract. It does not select the smallest factor, the most balanced rectangle, or the calculation listed first. Apple documents that successive task-group results arrive in completion order, independently of insertion order. TaskGroup.next(isolation:)
Define the behavior before writing the helper:
| Situation | Required behavior |
|---|---|
A calculation completes with nil |
Continue waiting for other calculations. |
| A calculation completes with a value | Select it and request cancellation of the remaining work. |
Every calculation returns nil |
Return nil. |
| The calculation list is empty | Return nil. |
| Parent cancellation is observed | Stop selecting results and return nil. |
The last row is a deliberate API choice for this example. Here, nil means that the invocation supplies no usable value; it does not distinguish an unsuccessful search from cancellation. If your caller needs that distinction, use an explicit outcome type or a throwing contract.
Build a complete local example on your Mac
Use a Swift 6 or newer toolchain. In Terminal, check it with swift --version. If Swift is unavailable, follow the official Install Swift — macOS instructions. The program uses Foundation and Swift’s concurrency APIs without external package dependencies.
Create a fresh executable package with these commands. This follows Swift.org’s documented package creation and execution workflow. Build a Command-line Tool
mkdir FirstNonNilDemo
cd FirstNonNilDemo
swift package init --name FirstNonNilDemo --type executable
Replace the generated executable source with the complete program below. Depending on the generated layout, it may be named Sources/FirstNonNilDemo/FirstNonNilDemo.swift or Sources/main.swift. Use one entry-point file named FirstNonNilDemo.swift; if the template created main.swift, rename that file before adding the @main program. Swift’s guide documents that a program uses either main.swift or an @main entry point. Build a Command-line Tool
import Foundation
func firstNonNil<Value: Sendable>(
_ calculations: [@Sendable () async -> Value?]
) async -> Value? {
guard !Task.isCancelled else { return nil }
return await withTaskGroup(
of: Value?.self,
returning: Value?.self
) { group in
defer { group.cancelAll() }
for calculation in calculations {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await calculation()
}
guard added else { break }
}
for await candidate in group {
guard !Task.isCancelled else { return nil }
if let value = candidate {
return value
}
}
return nil
}
}
func findFactor(total: Int, range: ClosedRange<Int>) -> Int? {
guard total > 1 else { return nil }
for rows in range {
guard !Task.isCancelled else { return nil }
guard rows > 1, rows < total else { continue }
if total % rows == 0 {
return rows
}
}
return nil
}
@main
struct FirstNonNilDemo {
static func main() async {
let checks: [@Sendable () async -> Int?] = [
{ nil },
{ 42 }
]
let checkResult = await firstNonNil(checks)
precondition(checkResult == 42)
let empty: [@Sendable () async -> Int?] = []
let emptyResult = await firstNonNil(empty)
precondition(emptyResult == nil)
for rawText in [" 221\n", " 223 ", "tiles"] {
let text = rawText.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard let total = Int(text), total > 1 else {
print("Input must be a whole number greater than 1.")
continue
}
let calculations: [@Sendable () async -> Int?] = [
{ findFactor(total: total, range: 2...5) },
{ findFactor(total: total, range: 6...16) },
{ findFactor(total: total, range: 17...30) }
]
if let rows = await firstNonNil(calculations) {
print("\(total) tiles: \(rows) rows x \(total / rows) columns")
} else {
print("\(total) tiles: no factor in the supplied ranges.")
}
}
}
}
Run the package from its directory:
swift run FirstNonNilDemo
For 221, either 221 tiles: 13 rows x 17 columns or 221 tiles: 17 rows x 13 columns is acceptable. For 223, the program reports no factor in the supplied ranges. The text tiles produces the input message. These are expected outcomes, not a recorded execution transcript. The arithmetic was checked separately; a Swift compiler was unavailable in the research environment, so the Swift program was inspected but not executed.
Foundation handles only the surrounding whitespace cleanup. Its trimmingCharacters(in:) method removes matching characters from both ends; it does not remove characters inside the string. Consequently, 221 becomes 221, while 2 21 remains invalid for this program. trimmingCharacters(in:)
Why the helper skips nil without losing later results
The group’s child-result type is Value?. Each child therefore supplies either a usable value or an ordinary empty result. The for await loop receives completed child results, and its inner optional binding selects only a non-nil candidate.
There are two different kinds of emptiness if you use group.next() directly. Its documented return type is ChildTaskResult?. With optional child results, that becomes Value??: the outer optional represents whether another child result exists, and the inner optional represents whether that child found a value. TaskGroup.next(isolation:)
This distinction explains a common mistake: taking one result and returning immediately. If the first completed calculation returns nil, other children may still have useful answers. The loop in the complete example keeps consuming results until it finds a value or the group is exhausted.
The helper uses addTaskUnlessCancelled and checks its Boolean return value. Apple documents that this method declines to create a child when the group is already canceled. Breaking the submission loop avoids continuing to submit work after that condition is detected. addTaskUnlessCancelled(priority:operation:)
The calculations capture the immutable tile count and return their answers through the group. They do not update a shared winner variable. The generic result must conform to Sendable, matching the documented task-group constraint. withTaskGroup(of:returning:isolation:body:)
Why selecting a winner does not mean returning immediately
Returning a value from the group closure selects the answer, but the enclosing withTaskGroup still waits for all children to finish. Apple explicitly documents that even canceled children must complete before the function returns. withTaskGroup(of:returning:isolation:body:)
The defer block calls group.cancelAll() whenever the closure exits. That requests cancellation of remaining calculations. Cancellation does not interrupt arbitrary computation: children must cooperate and finish early themselves. TaskGroup
In this example, findFactor checks Task.isCancelled before every candidate divisor. Each unit of work is small, so there is a frequent opportunity to stop. For a larger calculation, place checks between meaningful bounded chunks. A check only before a long indivisible operation leaves that operation running until it finishes. Swift’s language guide describes cancellation as cooperative and identifies returning nil as one supported response. Concurrency
If a competing calculation never finishes and never responds to cancellation, this structured helper cannot promise a prompt return. The supported path is to make each worker bounded or cancellation-aware. Adding a competing timeout result does not remove the group’s obligation to await its children.
Parent cancellation is checked before submission, at child entry, inside the arithmetic loop, and before accepting each result. These checks observe cancellation at particular moments; they do not create an atomic rule that cancellation always overrides a value. A cancellation arriving after the last check may coincide with a successful return.
Choose the right selection rule and verify its boundaries
The synthetic example provides a useful decision test: both 13 and 17 satisfy the stated contract. If your tool requires the smaller row count, accepting either result is incorrect. Completion order is useful only when every accepted answer is interchangeable for the reader’s task.
| What the caller needs | Appropriate approach |
|---|---|
| Any acceptable value as calculations finish | Use the task-group loop shown above. |
| The earliest successful calculation in a fixed preference list | Evaluate in preference order, or retain indexes and resolve earlier candidates before choosing. |
| The smallest or best-scoring result | Compare the relevant results before selecting. |
| Several known results needed together | Consider async let. |
Swift’s language guide presents async let for known child tasks whose results are needed later, and task groups for explicit control over dynamically added children. Awaiting named results in a chosen order does not implement a completion-order selection loop. Concurrency
Use these checks when adapting the program:
- Keep the empty-list assertion: it should return
nil. - Keep the mixed
niland42assertion: it should return42regardless of completion order. - Test calculations that all return
nil. - For multiple valid answers, assert membership in the valid set rather than a particular winner.
- Inspect every worker for bounded progress and cancellation checks.
- Report an unsuccessful search as limited to the supplied ranges; do not infer that no answer exists elsewhere.
The three tiny factor scans demonstrate behavior, not a speed improvement. A sequential scan is adequate for these illustrative numbers. Use the competing-calculation pattern when the actual task benefits from accepting interchangeable results, and measure that workload before choosing how much work to run concurrently.
Keep the acceptance rule explicit, skip empty child results, and make the remaining work able to stop. That gives the Mac tool a predictable value contract: the first usable result delivered by the group, followed by completion of the children’s cooperative shutdown.
Sources
- Install Swift — macOS — Swift.org
- Build a Command-line Tool — Swift.org
- TaskGroup — Apple
- withTaskGroup(of:returning:isolation:body:) — Apple
- next(isolation:) — Apple
- addTaskUnlessCancelled(priority:operation:) — Apple
- trimmingCharacters(in:) — Apple
- Concurrency — The Swift Programming Language, Swift.org