3

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?

aheze
  • 24,434
  • 8
  • 68
  • 125
Nicolas Gimelli
  • 695
  • 7
  • 19
  • You are actually leaving out a lot of code that you allude to, but don't show that is necessary to answer the question. Please just post a [Minimal Reproducible Example (MRE)](https://stackoverflow.com/help/minimal-reproducible-example). It will be easier for all. – Yrb Mar 28 '22 at 19:17
  • It is not an MRE. We should be able to copy your code and run it in Xcode ourselves. – Yrb Mar 28 '22 at 23:06
  • I've altered it. It should be copy-pastable. – Nicolas Gimelli Mar 29 '22 at 18:56
  • Hi @NicolasGimelli, could you elaborate the question? The subviews are currently horizontally centered (`HStack(alignment: .center)`), but you want them to be centered vertically (`VStack(alignment: .center)`) when any of them is tapped? Did I understand that right? I am not sure what is the role of `scrollTo` in your code though. – Cuneyt Apr 15 '22 at 00:23
  • Hi @Cuneyt, yes that is correct. The scrollTo was an attempt to “scroll” to the item that is clicked, thus centering it. Like I said, it did not work. – Nicolas Gimelli Apr 16 '22 at 04:44
  • Thanks @NicolasGimelli, but it's still not clear to me. Do you still want the scroll view to scroll horizontally after the tap? Do you want the views (all of them) to be drawn vertically after the tap? Do you want only the view which was tapped to be centred? If so, do you want that particular view to be drawn horizontally? If so, in what direction do you want that view to be centred? If you can draw the before the tap/after the tap views that would help. My understanding from your question is this: Before the tap: | Row --- Row --- Row | After the tap: |Row| |Row| |Row| – Cuneyt Apr 16 '22 at 05:24
  • What is `item.id`? – aheze Apr 19 '22 at 03:07

2 Answers2

2

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.

theMomax
  • 919
  • 7
  • 8
  • Can you suggest how to avoid last element to scroll in the center, or keep the last element at trailing position !? – Hitesh Patil Jan 10 '23 at 07:16
  • 1
    @Hitesh Patil just decrease the width of the bottom Color.clear‘s frame to your liking or remove the Color.clear entirely. – theMomax Jan 11 '23 at 08:19
0

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})
        
        )
        }
    }
}
Denzel
  • 309
  • 1
  • 8