skip to Main Content

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:

View Hierarchy Debugger Snapshot

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?

2

Answers


  1. First of all


    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

    enter image description here

    Then


    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

    Conclusion


    Your are just adjusting the HStack‘s position not the SlideView‘s.

    What’s more


    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))
    
    Login or Signup to reply.
  2. It’s working because each of the slides is being given the full width of the screen and they are all contained inside an HStack. This means, the width of the HStack is 3x the width of the screen, so at any time you are only seeing one third of the HStack and it is always the first third.

    The offset for all slides is being calculated as screenWidth * (1 - currentPage). This means:

    • when currentPage is 1, none of the slides have an offset, so the first slide is seen in the first third of the HSTack -> OK
    • when currentPage is 2, all slides have an offset of one screen’s width, so the second slide is seen in the first third of the HStack -> OK
    • when currentPage is 3, all slides have an offset of two screen’s width, so the third slide is seen in the first third of the HStack -> OK.

    Another way to do it would be to apply the offset to the HStack instead. The parameter should still be passed as 1 in this case too, so in fact the parameter is redundant and you can change the function to work without a parameter.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search