To sort a local Swift array by several fields, compare each key in priority order and continue only when it ties. Resolve optional fallback values separately, then define where missing values belong. This example orders synthetic shape labels for a Mac desktop list.

Define the sorting contract before writing the closure

Suppose a desktop list contains shape labels with a group number, an optional preferred position, an optional backup position, and a title. The task is to produce one predictable display order from these in-memory values.

Two decisions need separate rules. A fallback chooses which value represents one item: use its preferred position when present, otherwise its backup position. A tie-breaker compares another field only after two items have equal effective positions. Swift’s nil-coalescing operator, ??, supports the first decision by selecting the wrapped value or the fallback. Basic Operators — Nil-Coalescing Operator

Use this explicit contract for the example:

Priority Key Rule
1 Group Smaller numbers first
2 Effective position preferredPosition ?? backupPosition
3 Missing-position placement Missing positions follow present positions within each group
4 Title Ascending Swift string comparison when positions tie
5 Identifier Smaller identifiers first when every earlier key ties

Missing-position placement is part of the position comparison, rather than an independent field that overrides the group. An item in group zero still precedes an item in group one even if the first item has no position.

The title comparison deliberately uses Swift’s ordinary string ordering for this synthetic calculation. Keep that choice consistent if you later replace the closure with Foundation comparators.

Write a complete multi-field sort with explicit nil handling

The following complete program uses the Swift standard library and requires no imports. Run it in a Swift-capable environment on your Mac. All input is defined in the program; the calculation uses no external input or persistent storage. The sorting operation returns a new array through sorted(by:). Apple’s sorted(by:) documentation

struct ShapeItem {
    let id: Int
    let group: Int
    let preferredPosition: Int?
    let backupPosition: Int?
    let title: String

    var effectivePosition: Int? {
        preferredPosition ?? backupPosition
    }
}

func comesBefore(_ lhs: ShapeItem, _ rhs: ShapeItem) -> Bool {
    if lhs.group != rhs.group {
        return lhs.group < rhs.group
    }

    switch (lhs.effectivePosition, rhs.effectivePosition) {
    case let (.some(left), .some(right)):
        if left != right {
            return left < right
        }
    case (.some(_), .none):
        return true
    case (.none, .some(_)):
        return false
    case (.none, .none):
        break
    }

    if lhs.title != rhs.title {
        return lhs.title < rhs.title
    }

    return lhs.id < rhs.id
}

let items = [
    ShapeItem(id: 10, group: 1, preferredPosition: nil,
              backupPosition: 2, title: "Circle"),
    ShapeItem(id: 20, group: 1, preferredPosition: 1,
              backupPosition: 9, title: "Square"),
    ShapeItem(id: 30, group: 1, preferredPosition: nil,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 40, group: 1, preferredPosition: 2,
              backupPosition: 0, title: "Arc"),
    ShapeItem(id: 50, group: 0, preferredPosition: nil,
              backupPosition: nil, title: "Dot"),
    ShapeItem(id: 60, group: 1, preferredPosition: 2,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 70, group: 1, preferredPosition: 0,
              backupPosition: 8, title: "Zero")
]

let ordered = items.sorted(by: comesBefore)
let expectedIDs = [50, 70, 20, 40, 60, 10, 30]

precondition(ordered.map(\.id) == expectedIDs)
precondition(ordered.count == items.count)
precondition(items.reversed().sorted(by: comesBefore).map(\.id)
             == expectedIDs)

print(ordered.map(\.id))

Expected output:

[50, 70, 20, 40, 60, 10, 30]

The program and expected order have been inspected, but were not executed here because the research environment has no Swift compiler. The included preconditions let you check the result when running it locally.

Why each sample lands in that position

Item 50 comes first because group zero wins before position is considered. Item 70 then demonstrates that zero is a valid preferred position: its backup value of eight is unused. Item 20 uses its preferred position of one.

Items 40, 60, and 10 all have an effective position of two. The two Arc titles precede Circle, and identifiers resolve the remaining tie between 40 and 60. Item 30 comes last because both position fields are missing within group one.

This also answers a common fallback question: item 40 does not use its backup position of zero when its preferred position ties another item. Under this contract, the backup only replaces an absent preferred value. If the backup should also break ties, that would require an additional comparison rule.

Keep the comparator valid when values tie

Swift requires the sorting predicate to form a strict weak ordering. In practical terms, an item must never precede itself, ordering decisions must remain consistent across comparisons, and equivalent items must form consistent groups. Apple documents these requirements for sorted(by:). Predicate requirements

The example checks whether an earlier key differs before returning its ordering result. It reaches the title only when group and effective position tie, then reaches the identifier only when the title ties too. With equal identifiers and equal earlier keys, the final strict comparison returns false.

Avoid joining independent field comparisons with ||. One item could have an earlier group but a later position; another could have the opposite combination. An OR-based predicate could then say that each item belongs before the other. Sequential tie handling prevents that contradiction.

Also avoid using <= as the final comparison: equal values would return true, violating the rule that an item cannot precede itself. Keep each comparison dependent only on the supplied values and fixed sorting rules.

Choose between stable ties and a final identifier

Swift’s documented sort is stable: elements that the predicate leaves equivalent retain their relative input order. If that is the intended behavior, omit the final identifier comparison and return false after the title ties. Stable sorting guarantee

This example instead uses unique synthetic identifiers to resolve remaining ties. Consequently, reversing the sample input still produces the same identifier sequence. That property follows from this contract and sample values. If identifiers repeat alongside every other key, those items remain equivalent and retain their input order.

Express the same rules with Foundation key-path comparators

Foundation provides KeyPathComparator for comparing properties and sorted(using:) for applying a sequence of comparators. The first comparator supplies the primary order; subsequent comparators refine ties. These APIs are available starting with macOS 12. KeyPathComparator, sorted(using:)

For an equivalent Foundation version, expose two position keys: a missing-value rank and a numeric position. The rank puts missing positions last. Once that rank ties, a numeric substitute for missing positions cannot collide with a present zero, because those items already have different ranks.

Append the following extension and calculation after the complete program above. This is a dependent example, using its existing ShapeItem, items, and expectedIDs definitions. It requires macOS 14 or later because it explicitly constructs ComparableComparator with init(order:). Initializer availability

import Foundation

extension ShapeItem {
    var missingPositionRank: Int {
        effectivePosition == nil ? 1 : 0
    }

    var numericPositionKey: Int {
        effectivePosition ?? 0
    }
}

if #available(macOS 14.0, *) {
    let comparators: [KeyPathComparator<ShapeItem>] = [
        KeyPathComparator(\ShapeItem.group, order: .forward),
        KeyPathComparator(\ShapeItem.missingPositionRank,
                          order: .forward),
        KeyPathComparator(\ShapeItem.numericPositionKey,
                          order: .forward),
        KeyPathComparator(
            \ShapeItem.title,
            comparator: ComparableComparator<String>(order: .forward)
        ),
        KeyPathComparator(\ShapeItem.id, order: .forward)
    ]

    let foundationOrdered = items.sorted(using: comparators)
    precondition(foundationOrdered.map(\.id) == expectedIDs)
    print(foundationOrdered.map(\.id))
} else {
    print(items.sorted(by: comesBefore).map(\.id))
}

The explicit title comparator preserves the earlier choice of Swift string ordering. Foundation documents ComparableComparator as using the compared type’s Comparable implementation, and KeyPathComparator accepts a supplied property comparator. ComparableComparator, Key-path comparator initializer

For one fixed arrangement, the closure makes every branch visible. A comparator array is useful when you want the order of supported keys represented as values. In either approach, keep fallback selection in the model’s computed property so both implementations use the same definition.

Adapt the rules without changing their meaning

For descending positions while keeping missing positions last, change only the comparison between two present positions to left > right. In the Foundation version, reverse only numericPositionKey; leave missingPositionRank forward. Reversing the entire result would also reverse groups, titles, identifiers, and missing-value placement.

If an optional string becomes a fallback key, decide separately whether an empty string counts as missing. Nil coalescing responds to nil; it does not replace a present empty string. Resolve any additional normalization rule before sorting, and use that same effective key throughout. Nil-coalescing behavior

Keep this small calculation synchronous. Swift’s concurrency guide distinguishes ordinary synchronous sorting from suspension points, and the documented sorted(by:) predicate is synchronous. If an application needs values from asynchronous work, finish obtaining those values before calling this sorter. Adding await inside the comparison closure is not a supported extension of its signature. Concurrency, sorted(by:) signature

Before adapting the example to another desktop list, retain checks for a missing preferred key, two missing position keys, a valid zero, equal effective positions, and fully tied display fields. Decide whether the last tie should preserve input order or use an identifier. Those choices give the list a precise ordering contract that remains understandable in either implementation.

Sources