Search code examples
swiftswiftuiscrollviewhstack

Snap positioning for cells in SwiftUI Horizontal ScrollView


I have a horizontal scroll with some fixed size content cells, how can I make it so that it's a swipe to go to the next/previous cell? Similar to a tinder swipe but all remain in the scroll view. I want the cells to 'click' to the leading edge of the Hstack

struct ContentView: View {
        let content = ["Hello", "World", "How", "Are", "You"]
        
        var body: some View {
            ScrollView(.horizontal) {
                HStack {
                    ForEach(content) { str in
                        Text(str)
                            .frame(width: 200, height: 200, alignment: .center)
                            .background(Color.green)
                    }
                }
            }
        }
    }

Solution

  • Try applying .scrollTargetLayout() to the HStack and .scrollTargetBehavior(.paging) to the ScrollView. Also:

    • The ForEach had an error, using .enumerated() is a way to fix this.
    • Use .containerRelativeFrame(.horizontal) to set the width of each view to the width of the containing ScrollView.
    • It works best when the HStack has spacing: 0. So if you would like spacing between the pages, use padding to achieve it.
    • The id of the selected page can be tracked by using a .scrollPosition modifier.
    • An .onChange handler can be used to detect a change of page id. This is where you could trigger an impact effect (ref. your question in a comment).
    @State private var pageId: Int?
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack(spacing: 0) {
                ForEach(Array(content.enumerated()), id: \.offset) { offset, str in
                    Text(str)
                        .frame(height: 200, alignment: .center)
                        .frame(maxWidth: .infinity)
                        .background(
                            RoundedRectangle(cornerRadius: 10)
                                .fill(.green)
                        )
                        .padding(.horizontal, 10)
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .scrollPosition(id: $pageId)
        .scrollTargetBehavior(.paging)
        .onChange(of: pageId) { oldVal, newVal in
            if let newVal {
                print("page \(newVal) selected")
            }
        }
    }
    

    Animation