From c2f8aef6a0a0fc57a3ea3910b29602ce185bccf7 Mon Sep 17 00:00:00 2001 From: Alex Odawa Date: Thu, 4 Jun 2026 16:39:50 +0200 Subject: [PATCH 1/2] Plumb isSelectable into ApplyItemContentInfo Add an `isSelectable` property to `ApplyItemContentInfo`, populated from the item's `selectionStyle.isSelectable`. This lets content views know when an item is interactive (`.tappable`, `.selectable`, `.toggles`) so they can represent themselves accordingly, e.g. by applying the `.button` accessibility trait for VoiceOver. TBHFUZZ-165 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 + .../PresentationState.ItemState.swift | 1 + ListableUI/Sources/Item/ItemContent.swift | 7 ++- .../PresentationState.ItemStateTests.swift | 52 +++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b115c4ff..5be46fda9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Added +- Added `isSelectable` to `ApplyItemContentInfo`, reflecting whether the item's `selectionStyle` is interactive (`.tappable`, `.selectable`, or `.toggles`). `ItemContent` implementations can read this to represent themselves as interactive, for example by applying the `.button` accessibility trait so VoiceOver users know the item responds to taps. + ### Removed ### Changed diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift index 891687e75..e9586d355 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift @@ -312,6 +312,7 @@ extension PresentationState cell.openTrailingSwipeActions() }, isReorderable: self.model.reordering != nil, + isSelectable: self.model.selectionStyle.isSelectable, environment: environment ) diff --git a/ListableUI/Sources/Item/ItemContent.swift b/ListableUI/Sources/Item/ItemContent.swift index 93b8937b9..25f1113c7 100644 --- a/ListableUI/Sources/Item/ItemContent.swift +++ b/ListableUI/Sources/Item/ItemContent.swift @@ -565,7 +565,12 @@ public struct ApplyItemContentInfo /// If the item can be reordered. /// Use this property to determine if your `ItemContent` should display a reorder control. public var isReorderable : Bool - + + /// If the item is selectable; that is, if its `selectionStyle` is `.tappable`, `.selectable`, or `.toggles`. + /// Use this property to determine if your `ItemContent` should represent itself as interactive, for example + /// by applying the `.button` accessibility trait so VoiceOver users know the item responds to taps. + public var isSelectable : Bool = false + /// The environment of the containing list. /// See `ListEnvironment` for usage information. public var environment : ListEnvironment diff --git a/ListableUI/Tests/Internal/Presentation State/PresentationState.ItemStateTests.swift b/ListableUI/Tests/Internal/Presentation State/PresentationState.ItemStateTests.swift index a66cfbef6..c46194344 100644 --- a/ListableUI/Tests/Internal/Presentation State/PresentationState.ItemStateTests.swift +++ b/ListableUI/Tests/Internal/Presentation State/PresentationState.ItemStateTests.swift @@ -318,6 +318,58 @@ class PresentationState_ItemStateTests : XCTestCase XCTAssertEqual(state.coordination.coordinator?.willDisplay_calls.count, 1) XCTAssertEqual(state.coordination.coordinator?.didEndDisplay_calls.count, 1) } + + func test_applyTo_isSelectable() + { + XCTAssertEqual(appliedInfo(for: .tappable).isSelectable, true) + XCTAssertEqual(appliedInfo(for: .notSelectable).isSelectable, false) + } + + /// Builds an `ItemState` for the given `selectionStyle`, applies it to a cell, and returns the `ApplyItemContentInfo` passed to the content. + private func appliedInfo(for selectionStyle : ItemSelectionStyle) -> ApplyItemContentInfo + { + var applied : ApplyItemContentInfo? + + let item = Item(CapturingContent { applied = $0 }, selectionStyle: selectionStyle) + + let state = PresentationState.ItemState( + with: item, + dependencies: ItemStateDependencies( + reorderingDelegate: ReorderingActionsDelegateMock(), + coordinatorDelegate: ItemContentCoordinatorDelegateMock(), + environmentProvider: { .empty } + ), + updateCallbacks: UpdateCallbacks(.immediate, wantsAnimations: false), + performsContentCallbacks: true + ) + + state.applyTo( + cell: ItemCell(frame: .zero), + itemState: .init(isSelected: false, isHighlighted: false, isReordering: false), + reason: .willDisplay, + environment: .empty + ) + + return applied! + } +} + + +fileprivate struct CapturingContent : ItemContent +{ + typealias ContentView = UIView + + let onApply : (ApplyItemContentInfo) -> () + + var identifierValue : String { "" } + + func isEquivalent(to other : CapturingContent) -> Bool { false } + + func apply(to views : ItemContentViews, for reason : ApplyReason, with info : ApplyItemContentInfo) { + onApply(info) + } + + static func createReusableContentView(frame : CGRect) -> UIView { UIView(frame: frame) } } From 6279684f5fdc781d908bcc0a285bac262d435f6f Mon Sep 17 00:00:00 2001 From: Alex Odawa Date: Thu, 4 Jun 2026 17:54:52 +0200 Subject: [PATCH 2/2] Remove default value on isSelectable for parity with isReorderable Drops the `= false` default on the new public `isSelectable` property so it matches the sibling `isReorderable` (no default) and makes any future internal construction site that omits it a compile-time error rather than a silent wrong default. Co-Authored-By: Claude Opus 4.8 (1M context) --- ListableUI/Sources/Item/ItemContent.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ListableUI/Sources/Item/ItemContent.swift b/ListableUI/Sources/Item/ItemContent.swift index 25f1113c7..90e4af5a9 100644 --- a/ListableUI/Sources/Item/ItemContent.swift +++ b/ListableUI/Sources/Item/ItemContent.swift @@ -569,7 +569,7 @@ public struct ApplyItemContentInfo /// If the item is selectable; that is, if its `selectionStyle` is `.tappable`, `.selectable`, or `.toggles`. /// Use this property to determine if your `ItemContent` should represent itself as interactive, for example /// by applying the `.button` accessibility trait so VoiceOver users know the item responds to taps. - public var isSelectable : Bool = false + public var isSelectable : Bool /// The environment of the containing list. /// See `ListEnvironment` for usage information.