There seems to be a bug with SwiftUI's TabView when used with .tabViewStyle(.page) and any animated .transition() that moves the view horizontally (e.g. .move(edge: .leading/.trailing)), .slide, .offset(), etc.) in landscape orientation.
When the tab view transitions in, the content appears off-center and the animation goes back and forth before it stabilizes.
Did anyone else experience this and is there any known workaround?
import SwiftUI
struct ContentView: View {
@State private var showTabView = false
var body: some View {
VStack {
Button("Toggle TabView") {
showTabView.toggle()
}
Spacer()
if showTabView {
TabView {
Text("Page One")
Text("Page Two")
}
.tabViewStyle(.page)
.transition(.slide)
}
}
.animation(.default, value: showTabView)
.padding()
}
}
#Preview {
ContentView()
}
@main
struct TabViewTransitionBugApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Tested on Xcode 15.3 (15E204a), iOS 17.3.1 iPhone, iOS 17.4 Simulator.
Bug report FB13687638 filed with Apple.
Update 1
It seems to be related to safe areas as it doesn't happen on Touch ID devices.
Update 2
As suggested in the answer below, adding .ignoresSafeArea(edges: .horizontal) on the TabView, changing the animation to easeInOut and removing the .padding() from the VStack fixes the initial transition, but swiping between tabs is now off-center.
Update 3
With all changes from the answer below (https://stackoverflow.com/a/78167053/1104534) the UI still has issues. Unfortunately, it seems that the only "solution" is to use a different transition...




This behaviour certainly seems to be irregular. It reminds me of the issue described in iOS 17 SwiftUI: Color Views in ScrollView Using containerRelativeFrame Overlap When Scrolling, so I tried some similar workarounds.
It works a lot better with the following changes:
1. Ignore the horizontal safe area insets
2. Use
.easeInOutanimationThe animation of the page indicators seems to take a little longer than the animation of the transition. When the default animation effect of
.springis used, this is especially noticeable. It is less noticeable if.easeInOutis used:EDIT Some more tuning:
Ignoring the safe area insets will leave you with the issue that the tabs now fill the full width of the screen. If you want to enforce the safe areas in the usual way then a
GeometryReadercan be used to measure the screen width.I found that if negative padding is used to expand the width of the
TabViewby a large amount (overall width > twice the normal width) then the animation of theTabViewand the page indicators become synchronized!When the view slides out to the right, it pauses before fully disappearing. This can be hidden by combining the
slidetransition with.opacitytoo.So here is an updated of your example with all the changes applied: