4

I want to create a custom scroll that snaps views to middle But I can't figure out how to set the offset correctly.

This is the sample of the scroll:

struct contentView: View {

    @State var startOffset: CGFloat = 0
    @State var translationWidth: CGFloat = 0
    @State var offset: CGFloat = 0
    @State var index: Int = 0


    var body: some View  {
        GeometryReader { geo in
            HStack {

                ForEach(0..<5, id: \.self) { num in
                    Button(action: {
                    // some action
                    }) {
                        Circle()
                    }
                    .frame(width: geo.size.width)
                    .offset(x: -self.offset*CGFloat(self.index) + self.translationWidth)

                }

            }.frame(width: geo.size.width, height: 200)
                .offset(x: self.startOffset )
                .highPriorityGesture (
                    DragGesture(minimumDistance: 0.1, coordinateSpace: .global)
                    .onChanged({ (value) in
                         self.translationWidth = value.translation.width
                     })

                     .onEnded({ (value) in
                         if self.translationWidth > 40 {
                                self.index -= 1

                        } else if self.translationWidth < -40 {
                            self.index += 1
                        }
                        self.translationWidth = 0
                    })
            )

            .onAppear {
                withAnimation(.none) {
                    let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
                    self.offset = geo.size.width
                    self.startOffset = itemsWidth/2
                }
            }
        }
    }
}

It works but it is slightly off the middle, it doesn't scroll exactly in the middle and I don't understand why.

The problem occurs with the onAppear closure:

.onAppear {
    withAnimation(.none) {
        let itemsWidth: CGFloat = CGFloat(geo.size.width*5) - (geo.size.width)
        self.offset = geo.size.width
        self.startOffset = itemsWidth/2
    }
}

I might be missing some small pixels in my calculations.

UPDATE

So Apparently the HStack has a default value for spacing between views. so to fix it you should remove it like so:

 HStack(spacing: 0) {
    ....
 }
2
  • I assume this solution should be helpful. Commented Jun 17, 2020 at 18:08
  • My bad, I meant scroll that snaps its view to middle, I updated the post Commented Jun 17, 2020 at 18:18

2 Answers 2

2

TabView's PageTabViewStyle() is another option for scrolling views in SwiftUI that snap to the middle:

struct ContentView: View {
    
    var body: some View {
        TabView {
            ForEach(0..<5) { _ in
                Button(action: {
                    // some action
                }) {
                    Circle().frame(width: 200, height: 200)
                }
            }
        }
        .tabViewStyle(PageTabViewStyle())
    }
}

The result looks like this:

enter image description here

Sign up to request clarification or add additional context in comments.

2 Comments

This works great, is simple and feels completely native. The only problem I have is that I need my items to scroll in groups, e.g. 3 at a time. Let's say there are 7 items, then the desired (UIKit like) behavior would be to show the following groups of items when swiping repeatedly: 123, 456, 567, the last swipe swiping just far enough to make item 7 visible. I'm afraid that's impossible with this approach. :-( On iOS 17, this behavior is built into ScrollView with scrollTargetBehavior(.paging). But I need to support iOS 16, too.
Oh, I just realized, the second problem is you can't make the previous and next item peek in, which is also a requirement for me. :-(
1

So after a bit of experimenting, I realized the HStack is using a default spacing which is not equal to zero. It means that in my calculations I didn't include the spacing between items

so to fix this all you need to add is:

 HStack(spacing: 0) {
    ...
 }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.