I'm experiencing some unexpected behavior with a slide transition in a SwiftUI view.
I have a series of slides within an HStack that I want to animate through when a button is pressed.
I expected that I'd need to calculate individual offsets for each slide to make them appear and disappear correctly.
However, when using the same offset calculation for each slide, the transition still behaves correctly, and I'm trying to understand why.
Here's the relevant part of my code:
struct OnboardingView: View {
@StateObject private var viewModel = OnboardingViewModel()
func calculateOffset(_ index: Int, _ screenWidth: CGFloat) -> CGFloat {
let difference = index - viewModel.currentPage
return screenWidth * CGFloat(difference)
}
var body: some View {
GeometryReader { geometry in
let screenWidth = geometry.frame(in: .global).size.width
HStack(spacing: 0) {
SlideView1()
.frame(width: screenWidth)
.offset(x: calculateOffset(1, screenWidth))
SlideView2()
.frame(width: screenWidth)
.offset(x: calculateOffset(1, screenWidth))
SlideView3()
.frame(width: screenWidth)
.offset(x: calculateOffset(1, screenWidth))
}
}
.background(Color.blue.ignoresSafeArea())
.safeAreaInset(edge: .bottom) {
VStack {
Text("Current Page: \(viewModel.currentPage)")
Button("Next") {
withAnimation {
viewModel.currentPage += 1
}
}
}
}
}
}
class OnboardingViewModel: ObservableObject {
@Published var currentPage = 1
}
struct SlideView1: View {
var body: some View {
Text("Slide 1")
}
}
struct SlideView2: View {
var body: some View {
Text("Slide 2")
}
}
struct SlideView3: View {
var body: some View {
Text("Slide 3")
}
}
class OnboardingViewModel: ObservableObject {
@Published var currentPage = 1
}
As you can see, calculateOffset(1, width) is used for all slides, yet the view transitions correctly with each button press.
To provide more context, here's an image from the Xcode view hierarchy debugger showing all the slides lined up side by side, which seems to confirm that the layout is as expected for an HStack:
I'm puzzled as to why this works. My expectation was that each slide should have its own offset calculation based on its position in the stack.
Could someone help me understand the underlying mechanics of why the shared offset leads to the correct behavior?
Let's make it a lot more readable and viewable.
let screenWidth = geometry.frame(in: .global).size.width
let screenHeight = geometry.frame(in: .global).size.height
HStack(spacing: 0) {
SlideView1()
.frame(width: screenWidth, height: screenHeight)
.offset(x: calculateOffset(1, screenWidth))
SlideView2()
.frame(width: screenWidth, height: screenHeight)
.offset(x: calculateOffset(1, screenWidth))
SlideView3()
.frame(width: screenWidth, height: screenHeight)
.offset(x: calculateOffset(1, screenWidth))
}
we can have a check in Debug View Hierarchy
and that seems to be like that
we can adjust the code a little bit
HStack(spacing: 0) {
SlideView1()
.frame(width: screenWidth, height: screenHeight)
SlideView2()
.frame(width: screenWidth, height: screenHeight)
SlideView3()
.frame(width: screenWidth, height: screenHeight)
}
.offset(x: calculateOffset(1, screenWidth))
If we have a check for Debug View Hierarchy
again, you can find it totally the same.
Every time you click the button, the HStack
's position is changed
Your are just adjusting the HStack
's position not the SlideView
's.
I think your func calculateOffset
is not the same you want, you can change it to.
func calculateOffset(_ index: Int, _ screenWidth: CGFloat) -> CGFloat {
let difference = 1 - index
return screenWidth * CGFloat(difference)
}
and make it effect by
.offset(x: calculateOffset(viewModel.currentPage, screenWidth))