Add a SwiftUI SwipeMenu component#165
Conversation
SwipeMenu (iOS 18+) presents the same scrollable tab bar above a horizontally paging content area as SwipeMenuView, with a SwiftUI-native API: the selected page is a Binding<Int>, pages come from a view builder, and appearance is configured with the new SwipeMenuOptions. The onWillChangeIndex/onDidChangeIndex closures mirror the delegate paging callbacks and fire exactly once per move. The selection indicator and tab title colors are interpolated from the content's continuous scroll progress, so they track the finger during swipes, and the flexible tab bar scrolls to keep the indicator in view. The interpolation math lives in SwipeMenuGeometry, a pure nonisolated helper covered by unit tests, alongside defaults tests for SwipeMenuOptions. The example app gains a SwiftUI demo (menu, options form, and settings mapping, all sharing the existing SwipeMenuSettings model) reachable from a new floating button, the DocC catalog gains a "SwipeMenu in SwiftUI" article, and the READMEs and changelog document the SwiftUI support.
There was a problem hiding this comment.
Solid, well-tested addition (SwipeMenuGeometry's pure math is thoroughly unit tested, and SwipeMenuOptions/SwipeMenuSettings+SwiftUI mapping is covered too). Overall design and SwiftUI usage look sound — no concurrency/Sendable issues, no force-unwraps, no obvious security concerns.
Left one inline comment on SwipeMenu.commitScrollEnd(): when a drag interrupts a programmatic (tap/binding-driven) scroll and settles on a different page than intended, the code fires an onDidChangeIndex call reporting arrival at the intended page before firing a second will/did pair for the actual landed page. This means a single physical scroll settle can produce two onDidChangeIndex calls, one for a page transition that never actually appeared on screen — contradicting the "fires exactly once per move" doc guarantee. Worth fixing (use the actual landed value) or at least confirming this is intentional before merging, since consumers may rely on these callbacks for accurate page-view tracking/analytics.
… scroll commitScrollEnd() fired onDidChangeIndex with the intended selection and then a second will/did pair for the page the drag really settled on, so a single physical settle produced two didChange calls — the first for a transition that never appeared on screen. The settle handler now commits the landed page directly and fires one pair per move; when interrupted, the did's destination can differ from the one its will announced, which the doc comment now spells out.
There was a problem hiding this comment.
Verified the fix in 2b94e89: commitScrollEnd() now commits and reports the page the scroll actually landed on (rather than the intended selection) when a drag interrupts a programmatic move, so a single settle produces exactly one onDidChangeIndex call as documented. Traced through the interrupted-scroll scenario and the onChange(of: selection) re-entrancy guard — both behave correctly. This resolves the only issue raised in the previous review; no new problems found.
Summary
Adds a SwiftUI counterpart to
SwipeMenuView: aSwipeMenuview (iOS 18+) that presents the same scrollable tab bar above a horizontally paging content area, driven by a selection binding instead of a data source.SwipeMenu— pages come fromtitlesand a page view builder. Setting the binding is the equivalent ofjump(to:animated:), and theonWillChangeIndex/onDidChangeIndexclosures mirror theSwipeMenuViewDelegatepaging callbacks, firing exactly once per move (swipe, tab tap, or binding change).SwipeMenuOptions— a SwiftUI-native mirror ofSwipeMenuViewOptions(Color/Font/EdgeInsets). Safe-area and clipping knobs are intentionally omitted; SwiftUI containers own those (ignoresSafeArea,clipped).selectedFontdoes not affect layout, and the.flexiblebar auto-scrolls to keep the indicator in view (the counterpart ofTabView.focus(on:)).SwipeMenuGeometry— the interpolation, focus, and paging math extracted into a purenonisolatedhelper so it can be unit tested directly.SwiftUIMenuView+SwiftUIOptionsView) sharing the existingSwipeMenuSettingsmodel, presented full screen from a new floating button on the UIKit demo.The package still targets iOS 16: the SwiftUI surface is gated with
@available(iOS 18.0, *)because it builds onscrollTargetBehavior(.paging),onScrollGeometryChange,ScrollPosition, andColor.mix.New tests:
SwipeMenuGeometryTests(17) andSwipeMenuOptionsTests(5) in the package,SwipeMenuSettingsSwiftUITests(4) in the example. Verified in the simulator that the default flexible/underline layout matches the UIKit demo and that segmented/circle with a non-zero initial selection renders correctly.Checklist
xcodebuild test -scheme SwipeMenuViewControllerpasses locallyswift format lint --strictpasses and the tree is formatter-clean