SwiftUI TabView with .page style seems to allow user interaction to interrupt an in-flight page transition, leaving the UI in an inconsistent state.
Expected: the visible page always matches the bound selection.
Observed: if the user taps/drags the pager during a programmatic page change, the bound selection updates, but the visible page can remain the previous one. After that, subsequent page changes can “jump” (e.g. two pages at a time), because the visual page and selection have diverged.
Here is a GIF showing the issue:
In the GIF, I start with selection = 5 and page “Page 5” visible. I tap Prev and then interact with the pager mid-transition. The visible page remains “Page 5”, but the model has updated (selection = 4). Tapping Prev again then animates two pages to reach “Page 3”, which suggests the pager’s internal state is now out of sync with the binding.
Repro steps:
- Run the minimum reproducible example below.
- Tap Prev or Next to change
selection. - While the page transition is still in progress, quickly tap/drag on the pager.
- Observe that
selectionchanges but the visible page sometimes does not.
Minimum reproducible example
struct Page: View {
let index: Int
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 24)
.fill(Color(white: 0.92))
.padding(24)
Text("Page \(index)")
.font(.system(size: 48, weight: .bold, design: .rounded))
}
}
}
struct PagerDesyncReproView: View {
@State private var selection: Int = 0
private let pageCount = 6
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
Button("Prev") {
guard selection > 0 else { return }
selection -= 1
}
.buttonStyle(.bordered)
Button("Next") {
guard selection < pageCount - 1 else { return }
selection += 1
}
.buttonStyle(.borderedProminent)
Spacer()
Text("selection = \(selection)")
.font(.system(.caption, design: .monospaced))
.foregroundStyle(.secondary)
}
.padding(.horizontal)
TabView(selection: $selection) {
ForEach(0..<pageCount, id: \.self) { i in
Page(index: i)
.tag(i)
}
}
.animation(.default, value: selection)
.transition(.slide)
.tabViewStyle(.page(indexDisplayMode: .always))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
Is there a supported way to prevent this desynchronization (e.g. disable interaction until the page transition completes), or to detect cancellation/completion of the page transition so the binding can be kept consistent?

