Test iOS List Sorting Contracts on a Cloud Mac
A file list contains “Report2” and “Report10,” and two entries with the same name swap places again after a refresh. The problem may not be in the UI refresh logic: the comparator may not define a complete ordering. When running iOS tests on a cloud Mac, extract sorting from the view and validate the rules with a small sample first. That makes the issue easier to reproduce than guessing from a screen recording.
Define the sorting contract first
This article uses the following rules for a file list: sort names in ascending order, compare numbers in natural order, and ignore case and diacritics. When names compare as equal, sort by a persistent business identifier in ascending order. Fix the comparison locale so tests do not change with the machine’s settings.
This is not the default answer for every product. A user-facing contact list may need to follow the current language’s sort order, while a file list shared across devices may require a consistent order. Choose the requirements before implementing them; “looks right” is not an acceptance criterion.
A stable identifier must remain unchanged across refreshes. Array indices, arrival times, and newly generated UUIDs cannot break ties between entries with equal names. The data ingestion layer should also reject duplicate identifiers; otherwise, even the final sort key cannot distinguish the entries.
A stable sort only preserves the relative input order of entries with equal values. It cannot fix a server that returns them in a different order each time.
Extract the comparator into a pure function
Add the following file to the app target. The example explicitly uses en_US_POSIX to establish a fixed comparison policy. That does not mean this locale is suitable for displaying names in every language.
import Foundation
struct ListEntry {
let id: String
let name: String
}
func entryLess(_ lhs: ListEntry, _ rhs: ListEntry) -> Bool {
let order = lhs.name.compare(
rhs.name,
options: [.numeric, .caseInsensitive, .diacriticInsensitive],
range: nil,
locale: Locale(identifier: "en_US_POSIX")
)
if order != .orderedSame {
return order == .orderedAscending
}
return lhs.id < rhs.id
}
Do not read network state, modify counters, or query user settings that may change during comparison. A single sort compares the same objects multiple times, so the results must be consistent. Do not implement the comparator with “less than or equal,” either: an object cannot come before itself.
Equivalent names do not imply identical business objects. Under this policy, “Resume” and “résumé” may fall into the same group, but that is not a reason to merge their records. Sorting, searching, and deduplication are three separate contracts.
Lock down key behavior with a small sample
Import the app module in your unit test file, then add the test class below. Place the code in a test target that can access the types defined above, and extend the sample in ListSortingTests with your own filenames.
import XCTest
final class ListSortingTests: XCTestCase {
let items = [
ListEntry(id: "d", name: "Report10"),
ListEntry(id: "b", name: "report2"),
ListEntry(id: "a", name: "REPORT2"),
ListEntry(id: "c", name: "Report1")
]
func testNaturalOrderAndTies() {
XCTAssertEqual(
items.sorted(by: entryLess).map(\.id),
["c", "a", "b", "d"]
)
}
func testComparatorProperties() {
for a in items {
XCTAssertFalse(entryLess(a, a))
for b in items {
if entryLess(a, b) {
XCTAssertFalse(entryLess(b, a))
}
for c in items {
if entryLess(a, b) && entryLess(b, c) {
XCTAssertTrue(entryLess(a, c))
}
}
}
}
}
func testInputOrderDoesNotChangeOutput() {
let expected = items.sorted(by: entryLess).map(\.id)
XCTAssertEqual(
Array(items.reversed()).sorted(by: entryLess).map(\.id),
expected
)
let rotated = Array(items.dropFirst()) + [items[0]]
XCTAssertEqual(
rotated.sorted(by: entryLess).map(\.id),
expected
)
}
}
The first test checks numeric ordering and ties between names. The second checks irreflexivity, asymmetry, and transitivity, catching contradictions introduced by ad hoc combinations of rules. The third changes the input order to verify that the result does not depend on arrival order.
The triple loop is appropriate for a dozen or so representative samples. Do not apply it directly to the full production list. Measure sorting time separately for large datasets; a property check is not a performance benchmark.
Cover Unicode and missing-value edge cases
Add empty names, leading zeros, Chinese names, combining accents, and emoji sequences. A precomposed character and a letter followed by a combining accent may look identical. String length cannot determine their display order.
For “File02” and “File2,” do not assume what numeric comparison will return. First decide whether the product treats them as belonging to the same group, then verify the behavior in the chosen runtime. If leading zeros must be distinguished, add an explicit secondary name rule when the initial name comparison returns equality. The business identifier should still be the final tie-breaker.
Handle missing names consistently during model conversion—for example, by converting them to empty strings or defining a branch that places missing entries last. Do not supply arbitrary defaults inside the comparator. Descending order also requires redefining the direction of the keys; simply negating the ascending comparison result would make equal objects compare as “less than.”
Run in the cloud and record failure evidence
In the test job on the cloud Mac, pin Xcode, the simulator runtime, and the comparison locale, then run these tests through your existing unit test entry point. Save the commit identifier, toolchain version, failing input, and actual output sequence of identifiers. There is no need to record real customer filenames.
Validate the pure function first, then the UI. Refresh twice in succession and confirm that entries with equal names do not swap places. Delete and reinsert one entry, then check that the relative order of the remaining entries follows the rules. The UI layer must reuse the same comparator rather than implement another sorting scheme.
Fixing the locale does not guarantee that system collation rules will never change. After upgrading the runtime, review representative language samples. If a difference comes from system rules, decide whether to accept the change or adopt custom sort keys instead of simply updating every snapshot.
The final deliverable should include the sorting contract, representative samples, comparator property checks, and refresh acceptance results. The next time the order changes, you can determine whether the data, rules, or runtime changed instead of starting over with guesses from screenshots.
Frequently asked questions
Does a stable sort prevent equal-name items from moving?
Not by itself. A stable sort preserves their relative input order. If server responses arrive in a different order, use a persistent, unique business ID as the final sorting key.
Does fixing the locale make a sorting snapshot permanent?
No. System collation rules can change with runtime updates. Recheck representative examples and comparator properties when upgrading the runtime.
Reserve a cloud Mac for your next build
Choose a model, location, and rental period, then run your own development tools on a dedicated physical node. Availability is confirmed in real time by the console.