TabView page control element has a bug on iOS 16 if tabview is configured as RTL with PageTabViewStyle.
Found iOS 16 Issues:
- Page indicators display dots in reverse order (appears to treat layout as LTR while showing RTL)
- Index selection is reversed - tapping indicators selects wrong pages
- Using the page control directly to navigate eventually breaks the index binding
- The underlying index counting logic conflicts with the visual presentation
iOS 18 Behavior:
Works as expected with correct dot order and index selection.
Xcode version:
Version 16.3 (16E140)
Conclusion:
- Confirmed broken on iOS 16
- Confirmed working on iOS 18
- iOS 17 and earlier versions not yet tested
I've opened a feedback assistant ticket quite a while ago but there is no answer. There's a code example and a video there.
Anyone else had experience with this particular bug?
Here's the code:
public struct PagingView<Content: View>: View {
//MARK: - Public Properties
let pages: (Int) -> Content
let numberOfPages: Int
let pageMargin: CGFloat
@Binding var currentPage: Int
//MARK: - Object's Lifecycle
public init(currentPage: Binding<Int>,
pageMargin: CGFloat = 20,
numberOfPages: Int,
@ViewBuilder pages: @escaping (Int) -> Content) {
self.pages = pages
self.numberOfPages = numberOfPages
self.pageMargin = pageMargin
_currentPage = currentPage
}
//MARK: - View's Layout
public var body: some View {
TabView(selection: $currentPage) {
ForEach(0..<numberOfPages, id: \.self) { index in
pages(index)
.padding(.horizontal, pageMargin)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.ignoresSafeArea()
}
}
//MARK: - Previews
struct ContentView: View {
@State var currentIndex: Int = 0
var body: some View {
ZStack {
Rectangle()
.frame(height: 300)
.foregroundStyle(Color.gray.opacity(0.2))
PagingView(
currentPage: $currentIndex.onChange({ index in
print("currentIndex: ", index)
}),
pageMargin: 20,
numberOfPages: 10) { index in
ZStack {
Rectangle()
.frame(width: 200, height: 200)
.foregroundStyle(Color.gray.opacity(0.2))
Text("\(index)")
.foregroundStyle(.brown)
.background(Color.yellow)
}
}.frame(height: 200)
}
}
}
#Preview("ContentView") {
ContentView()
}
extension Binding {
@MainActor
func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler(newValue)
}
)
}
}