Nicolas Gimelli
Nicolas Gimelli

Reputation: 1302

SwiftUI - How to center a component in a horizontal ScrollView when tapped?

I have a horizontal ScrollView, and within it an HStack. It contains multiple Subviews, rendered by a ForEach. I want to make it so that when these Subviews are tapped, they become centered vertically in the view. For example, I have:

ScrollView(.horizontal) {
    HStack(alignment: .center) {
        Circle() // for demonstration purposes, let's say the subviews are circles
            .frame(width: 50, height: 50)
        Circle()
            .frame(width: 50, height: 50)
        Circle()
            .frame(width: 50, height: 50)
    }
    .frame(width: UIScreen.main.bounds.size.width, alignment: .center)
}

I tried this code:

ScrollViewReader { scrollProxy in
    ScrollView(.horizontal) {
        HStack(alignment: .center) {
            Circle()
                .frame(width: 50, height: 50)
                .id("someID3")
                .frame(width: 50, height: 50)
                .onTapGesture {
                    scrollProxy.scrollTo(item.id, anchor: .center)
                }
            Circle()
                .frame(width: 50, height: 50)
                .id("someID3")
                .frame(width: 50, height: 50)
                .onTapGesture {
                    scrollProxy.scrollTo(item.id, anchor: .center)
                }

            ...
        }
    }

But it seemingly had no effect. Does anyone know how I can properly do this?

Upvotes: 3

Views: 3112

Answers (3)

Aarfeen Ahmad
Aarfeen Ahmad

Reputation: 76

If you are targeting iOS 17.0+, you can use safeAreaPadding

here is an example

GeometryReader { reader in
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack {
                   //....
                }
            }
            .safeAreaPadding(.horizontal, (reader.size.width - CELL_WIDTH) / 2)
        }

Upvotes: 0

theMomax
theMomax

Reputation: 1059

You can definitely do this with ScrollView and ScrollViewReader. However, I see a couple of things that could cause problems in your code sample:

  • You use the same id "someID3" twice.
  • I can't see where your item.id comes from, so I can't tell if it actually contains the same id ("someID3").
  • I don't know why you have two frames with the same bounds on the same view area. It shouldn't be a problem, but it's always best to keep things simple.

Here's a working example:

import SwiftUI

@main
struct MentalHealthLoggerApp: App {
    var body: some Scene {
        WindowGroup {
            ScrollViewReader { scrollProxy in
                ScrollView(.horizontal) {
                    HStack(alignment: .center, spacing: 10) {
                        Color.clear
                            .frame(width: (UIScreen.main.bounds.size.width - 70) / 2.0)
                        ForEach(Array(0..<10), id: \.self) { id in
                            ZStack(alignment: .center) {
                                Circle()
                                    .foregroundColor(.primary.opacity(Double(id)/10.0))
                                Text("\(id)")
                            }
                            .frame(width: 50, height: 50)
                            .onTapGesture {
                                withAnimation {
                                    scrollProxy.scrollTo(id, anchor: .center)
                                }
                            }
                            .id(id)
                        }
                        Color.clear
                            .frame(width: (UIScreen.main.bounds.size.width - 70) / 2.0)
                    }
                }
            }
        }
    }
}

Here you can see it in action:

[EDIT: You might have to click on it if the GIF won't play automatically.]

You can see the animated scrolling effect.

Note that I added some empty space to both ends of the ScrollView, so it's actually possible to center the first and last elements as ScrollViewProxy will never scroll beyond limits.

Upvotes: 4

Denzel
Denzel

Reputation: 323

Created a custom ScrollingHStack and using geometry reader and a bit calculation, here is what we have:

struct ContentView: View {

    var body: some View {
        ScrollingHStack(space: 10, height: 50)
    }
    
}

struct ScrollingHStack: View {
    
    var space: CGFloat
    var height: CGFloat
    var colors: [Color] = [.blue, .green, .yellow]
    
    @State var dragOffset = CGSize.zero
    
    var body: some View {
        GeometryReader { geometry in
        HStack(spacing: space) {
            ForEach(0..<15, id: \.self) { index in
                
                Circle()
                    .fill(colors[index % 3])
                    .frame(width: height, height: height)
                    .overlay(Text("\(Int(dragOffset.width))"))
                    .onAppear {
                        dragOffset.width = geometry.size.width / 2  - ((height + space) / 2)
                    }
                    .onTapGesture {
                        let totalItems = height * CGFloat(index)
                        let totalspace = space * CGFloat(index)
                        withAnimation {
                            dragOffset.width = (geometry.size.width / 2) - (totalItems + totalspace) - ((height + space) / 2)
                        }
                    }
            }
        }
        .offset(x: dragOffset.width)
        .gesture(DragGesture()
                    .onChanged({ dragOffset = $0.translation})
        
        )
        }
    }
}

Upvotes: 0

Related Questions