Hi everyone,
I’m having trouble getting the correct horizontal slide transitions when navigating between multiple screens in my SwiftUI app. I have three screens (enum cases with an int assigned as index depending on the navigation order): .checkClient (index 0), .login(document: String) (index 1), and .register (index 2).
My goal is:
- When moving forward (e.g., from .checkClient to .login, or from .login to any other with a greater index), the new screen should enter from the right (trailing) and the old one should exit to the left (leading).
- When going backward (from .register back to .checkClient, for example), the new screen should enter from the left (leading) and the old one should exit to the right (trailing).
I’ve been using a state property isAdvancing to determine the direction of transitions (I use TCA, so my logic is in a Reducer body, my properties in a State and my views are normal SwiftUI Views):
case .updateCurrentScreen(let newScreen):
state.isAdvancing = newScreen.index > state.currentScreen.index
state.currentScreen = newScreen
return .none
I tried applying .transition directly inside each case:
.transition(
.asymmetric(
insertion: .move(edge: store.isAdvancing ? .trailing : .leading),
removal: .move(edge: store.isAdvancing ? .leading : .trailing)
)
)
This works correctly the first time I navigate forward. However, when I go to the .register screen and then hit back, the directions become inconsistent. Sometimes the removal happens in the wrong direction, and after returning to .checkClient, forward navigations stop working as intended.
Then, I tried placing the transition at a higher level, wrapping the switch in a ZStack and using a single .transition(...) outside:
ZStack {
switch store.currentScreen {
case .checkClient:
StartView(...)
case .login:
LoginView(...)
case .register:
RegisterView(...)
}
}
.transition(
.asymmetric(
insertion: .move(edge: store.isAdvancing ? .trailing : .leading),
removal: .move(edge: store.isAdvancing ? .leading : .trailing)
)
)
.animation(.easeInOut, value: store.currentScreen)
But doing this results in some transitions reverting to a fade instead of a horizontal slide.
I’ve also tried ensuring that isAdvancing updates before changing the currentScreen. Unfortunately, I still encounter inconsistent transitions when navigating back and forth between these screens.
Here is my complete view logic (even though is not finished nor polished yet):
var body: some View {
WithPerceptionTracking {
ZStack {
AdaptiveSheetView(
backgroundImage: Asset.AuthorizationDomain.background,
hasBackButton: store.showsBackButton,
isFullScreen: store.isFullScreen,
backAction: {
store.send(.goBack)
}
) {
if store.showsIATILogo {
Image(asset: Asset.iatiLogo)
.padding(spacing: .medium)
}
ZStack {
switch store.currentScreen {
case .checkClient:
StartView(store: store.scope(state: \.startState, action: \.startAction))
case .login:
if let newStore = store.scope(state: \.loginState, action: \.loginAction) {
LoginView(store: newStore)
}
case .register:
if let newStore = store.scope(state: \.registerState, action: \.registerAction) {
RegisterView(store: newStore)
}
}
}
.transition(
.asymmetric(
insertion: .move(edge: store.isAdvancing ? .trailing : .leading),
removal: .move(edge: store.isAdvancing ? .leading : .trailing)
)
)
}
.animation(.easeInOut, value: store.currentScreen)
if store.startState.checkUser == .loading { LoadingSpinner() }
PreloadView(store: store.scope(state: \.preloadState, action: \.preloadAction))
.opacity(store.preloadViewShown ? 1.0 : 0.0)
.animation(.linear(duration: 0.5), value: store.preloadViewShown)
.onChange(of: store.preloadViewShown) { shown in
if !shown { store.send(._checkPreviousSessions) }
}
}
}
}
Has anyone experienced similar issues or found a reliable pattern for achieving these “push/pop” style transitions in SwiftUI? Any guidance would be greatly appreciated!
My minimum target is iOS 16, so I can not make use of TabView with paginated style for this AFAIK.
Thanks in advance for any time and attention you dedicate to me 🙏🏼