Robin Winters

A SwiftUI search adapter needs its own request ownership

Robin Winters · October 3, 2026 · Native iOS engineering and fitness technology

This standalone teaching example and article were prepared with coding-assistant support. The events are synthetic. The code is separate from ShowFlex, whose shipped iPhone product is available on the App Store.

A search controller can correctly reject stale requests while its view adapter still publishes the wrong state. The useful question is not only whether the controller owns its result: it is whether the task waiting for that result still owns the screen.

The public controller already checks a revision before publishing success, failure or cleanup. Its new native SwiftUI demo makes the surrounding adapter explicit. It searches three synthetic fitness-event titles, selects by identifier and exposes clear, cancel, error/retry and a deliberately troublesome request race.

Keep the observable owner stable

The controller has plain state properties. The adapter conforms to ObservableObject, publishes the state the interface reads and owns the controller. The view holds the adapter with @StateObject. These are established SwiftUI state-object and Combine observable-object mechanisms; this example uses them to preserve the declared iOS 16 minimum.

Starting a search immediately copies the controller's loading state, empty results and cleared selection. When the returned task settles, the adapter copies the completed state only if its own version still matches the version captured when it started waiting:

version += 1
let ownedVersion = version
let pending = controller.search(query)
publish()

Task { [weak self] in
    await pending?.value
    guard let self, self.version == ownedVersion else { return }
    self.publish()
}

This excerpt shows the ownership rule; the full source also handles an empty query before creating the waiting task. The adapter version protects its activity text and state-copying path. The controller revision protects the underlying results. The two checks sit at different boundaries.

Make the race difficult on purpose

The Race demo control starts a slow request, allows it to enter the fixture fetcher and then starts a fast request. The slow fetch uses a continuation resumed by a dispatch timer. Cancelling its Swift task does not cancel that timer. It really completes after the replacement request, instead of disappearing in a cooperative mock.

The accepted result is the fast request. The integration check waits for that result, then waits past the slow completion and verifies that the query and visible adapter results still belong to the fast request. This tests the published state that the SwiftUI list reads, rather than only the controller's internal value.

Selection and leaving the screen are separate policies

Selecting a result passes its identifier to the controller and republishes the resolved selection. Starting another query clears it. Cancel invalidates the adapter's waiting tasks, cancels the scheduled race transition and tells the controller that pending work no longer owns the surface. The view calls that method when it disappears.

Those are this demo's policies. A production search flow may retain old results during refresh or restore a selection when returning to a screen. Either choice needs a corresponding state model; it should not arise accidentally from a late completion.

Reproduce the checks

From the example directory, with Xcode on an Apple silicon Mac:

swift test
sh Demo/check-model.sh
sh Demo/build-simulator.sh

The seven controller tests and four executable adapter checks passed on macOS on October 3, 2026. The adapter checks cover the non-cooperative race, identity selection and clearing, current error and successful retry, and cancellation after a fetch has started. The ARM64 iOS simulator app compiled, installed and launched on an isolated iPhone 17 Pro running iOS 27.0. A compiler sysroot warning was emitted. At this initial build checkpoint, UI inspection, recording, accessibility, live-network behavior and physical-device execution were unverified; the later partial inspection is documented below.

Observed iPhone simulator interaction — October 3, 2026

A later inspection in Xcode Device Hub showed the initial screen on the isolated iPhone 17 Pro simulator running iOS 27.0. Tapping Strength returned one synthetic Strength meet result. Selecting it showed both the checkmark and the Selection section. These are actual, unaltered simulator-window screenshots, captured with coding-assistant support.

Initial native SwiftUI teaching demo in Xcode Device Hub, with empty results and search controls
Initial state: synthetic event search controls and no selected result.
Strength search result selected, with a checkmark and Strength meet displayed in the Selection section
Observed search and selection: one Strength meet result, selected by identity.

This partial inspection adds evidence beyond build and launch. It does not complete the race, clear, cancel, error/retry, scrolling or accessibility review. Recording, live-network and physical-device behavior remain unverified. The separate macOS checks above cover controller and adapter contracts.

The code is MIT licensed within the teaching example. It grants no access or license to private ShowFlex code. Build and test evidence belongs to this example; the separate App Store listing is evidence of the shipped ShowFlex product.

Code and executable checks · Original controller explanation · Robin Winters professional record · robin.ac