0

I have a SwiftUI ScrollView that contains a LazyVStack of tappable elements. When I tap on an element I wish to display a small popover at a position relative to that of the tapped element, regardless of the ScrollView offset. The popover position should be consistent between targets and not simply based on the screen location.

The actual design involves blurring the entire screen on the tap and presenting a new layer with a copy of the tapped element and the popover on a new, unblurred, layer. This means that an overlay-based approach won't work in this case.

I have a solution but it feels heavyweight and wonder if I've missed a native version.

My ScrollView is wrapped into an OffsetReporting version (adapted from the version presented here), so I know its current offset whenever the user scrolls. My elements have a GeometryReader overlay that gets their position. When elements are created I can combine the ScrollView offset and the element position and store the two as the absolute position of the element within the ScrollView. When I later need to know the position on screen I can take the current ScrollView offset and adjust the stored element position accordingly.

The broad strokes are presented below. Is there a simpler or more native way of achieving the same?

// struct and other wrapping code elided 
OffsetReportingScrollView(
    .vertical,
    showsIndicators: false,
    offsetChanged: { offset in
        // Store the ScrollView's offset.
    })
{
    ZStack {
        LazyVStack {
            ForEach(0...100, id: \.self) { (i: Int) in
                HStack {
                    Button {
                        // Enable the popover at the correct position via a property.
                    } label: {
                        Text("Item \(i)")
                    }
                    .buttonStyle(.plain)
                }

                // Capture the element's position on creation                
                .overlay {
                    GeometryReader { geometry in
                        Rectangle()
                            .foregroundColor(Color.clear)
                            .onAppear {
                                // Store the element's position w.r.t. the ScrollView's current offset.
                            }
                    }
                }
            }
        }
    }
}
Robin Macharg
  • 1,468
  • 14
  • 22
  • Does this answer your question https://stackoverflow.com/a/62588295/12299030? – Asperi Jul 04 '22 at 16:04
  • Thanks but I don't believe so, no. I already have the ScrollView offset but want the position of individual items when I tap on them. – Robin Macharg Jul 04 '22 at 16:19
  • are you aware of the new `.onTapGesture { location in` for iOS16? – ChrisR Jul 04 '22 at 20:19
  • I am now, thanks! But that only gives me the tap location, not the geometry of the tap target. I'd like to have the popup appear at a consistent position relative to each list element. I'll clarify the question. – Robin Macharg Jul 04 '22 at 21:32

1 Answers1

0

It might be easier to just add an overlay to the selected(tapped) element. Here is a sketch:

enter image description here

struct Item: Identifiable, Equatable {
    let id = UUID()
    let name: String
}

struct Model {
    var items: [Item]
    init() {
        items = []
        for i in 0..<100 {
            items.append(Item(name: "Item \(i)"))
        }
    }
}

struct ContentView: View {

    let data = Model()
    
    @State private var selectedItem: Item? = nil
    
    var body: some View {
        ScrollView {
            ForEach(data.items) { item in
                Button(item.name) {
                    selectedItem = item
                }
                .frame(maxWidth: .infinity)
                .padding(.horizontal)
                .overlay(
                    Text("Info for \(item.name)")
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(.gray)
                        .opacity(selectedItem == item ? 0.9 : 0)
                        .zIndex(1)
                )
            }
        }
   }
}
ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • Thanks for this, and it's a nice solution in the general case. For simplicity I skipped some of the detail in my question that may have led to assumptions. For instance I actually blur the entire screen and present an unblurred version of the tapped element - with an associated popover - on top. For this I need the tapped element's on-screen geometry to use as an offset for the popover. I'll make my question clearer. FWIW, the approach I suggested in the question is actually working out quite well. – Robin Macharg Jul 06 '22 at 09:33