App Development with Swift study materials
App Development with Swift study resources

The Clamp That Works on One Screen Only

App Development with Swift · Put your knowledge to work in context.

All case studies

The Clamp That Works on One Screen Only

Your team ships a dashboard with a filter panel reused on three screens. DashboardFilters is a struct with dateRange: DateRange (another struct), sources: [Source], and includeArchived: Bool. One FiltersStore class holds var current: DashboardFilters. A helper, PresetEngine.normalized(_:), returns a corrected copy in which end is clamped to be at least start. On the Analytics screen the clamp visibly works; on Orders and Reports the panel still shows an end date before the start date and the clamp appears to do nothing. Meanwhile, an array of eight savedPresets values is edited in place through its subscript, and those edits do persist. A junior developer concludes that structs are unreliable for shared state and proposes converting every model type to a class.

1. Explain, from value semantics, why the clamp persists on one screen but not the other two, and why the subscript edits do persist. Name the most likely call-site difference.

Read the worked answer

normalized(_:) is a pure function. It receives a copy, clamps that copy, and returns it; nothing in its signature can reach the caller's storage. The one-works-two-fail pattern is the fingerprint of a call-site difference, not a language defect.

Analytics almost certainly writes the result back:

store.current = PresetEngine.normalized(store.current)

Orders and Reports likely call it for a side effect and drop the return value, leaving a bare expression statement. Swift warns 'result of call is unused' here - so either the function is marked @discardableResult or someone is ignoring warnings, and either way the bug hides silently.

The subscript edit is a different mechanism. savedPresets is a var array, and savedPresets[i].dateRange.start = … is a mutating access. The write travels through the array's storage and persists. That is exactly why in-place mutation feels reliable while a pure helper does not.

Value semantics did not fail. Two call sites discarded a return value, and the type system let them.

2. The junior developer wants classes everywhere. Evaluate that proposal against the two symptoms, and propose a design that makes the mistake harder to repeat.

Read the worked answer

Classes would not have prevented either symptom, and would add new ones. A normalized method returning a new instance is still ignorable, so the call-site bug survives unchanged. What does change: every reference to DashboardFilters becomes shared, so editing filters on Orders would silently mutate the Analytics panel - precisely the aliasing the structs prevented. You also surrender cheap value equality (valuable for diffing and test assertions), simple thread-safety, and copy-on-write behaviour on sources.

Better options, in order:

  1. Move the operation onto the store as mutating func clampDates() or func applyPreset(_:) that assigns internally, so a caller cannot forget.
  2. If the helper must stay pure, name it clampingDates() and delete any @discardableResult, so the compiler flags dropped results.
  3. Keep the structs and add a store-level test.

A reference type is justified only if filters genuinely need shared identity across screens, such as live collaborative editing - and even then an observable class wrapping a struct value is safer than a bare class with public stored properties.

3. Write the regression test that would have caught this, and state why testing `PresetEngine.normalized` would not.

Read the worked answer

Test the store, not the helper:

func testApplyingPresetClampsDatesOnStore() {
    let store = FiltersStore(current: .fixture(start: d(10), end: d(5)))
    store.applyPreset(.lastSevenDays)
    XCTAssertGreaterThanOrEqual(store.current.dateRange.end,
                                store.current.dateRange.start)
}

normalized(_:) already returns a correctly clamped copy today; a unit test of it passes while the app is broken. The defect lives at the boundary between the pure function and the mutable store, so the assertion must sit on that boundary - or be a UI test exercising all three screens.

Corollary: if you keep the pure API, get a compile-time guard instead. Remove @discardableResult so every ignored result is a warning, and treat warnings as errors in CI. That converts a runtime bug into a build failure, which is the only kind of fix that stays fixed.