admin管理员组

文章数量:1356815

I am working with a SwiftUI List on macOS, and I have a popover view that I need to show from each row. I have an implementation that works but it shows the popover from one fixed point on the row. I would like to be able to show the popover from where the mouse was clicked in the row, with the arrow pointing to the mouse location.

This is my current implementation:

import SwiftUI

struct TestPopoverView: View {
    let items = ["Row 1", "Row 2", "Row 3", "Row 4", "Row 5"]

    
    var body: some View {
        List(items, id: \.self) { item in
            ListRowView(item: item)
        }
    }
}

struct ListRowView: View {
    let item: String
    @State private var showPopover = false
    
    var body: some View {
        HStack {
            Text(item)
                .padding()
            Spacer()
        }
        .contentShape(Rectangle()) // Ensures the entire row is tappable
        .onTapGesture {
            showPopover = true
        }
        .popover(isPresented: $showPopover, attachmentAnchor: .rect(.bounds), arrowEdge: .bottom) {
            PopoverContentView(item: item)
        }
    }
}

struct PopoverContentView: View {
    let item: String

    var body: some View {
        VStack {
            Text("Selected: \(item)")
                .padding()
        }
        .frame(width: 200, height: 100)
    }
}


#Preview {
    TestPopoverView()
        .frame(width: 300, height: 350)
}

How do I get the mouse location and the popover showing from that precise location? I don't understand how to specify the attachmentAnchor to do this, while working inside a List.

I am working with a SwiftUI List on macOS, and I have a popover view that I need to show from each row. I have an implementation that works but it shows the popover from one fixed point on the row. I would like to be able to show the popover from where the mouse was clicked in the row, with the arrow pointing to the mouse location.

This is my current implementation:

import SwiftUI

struct TestPopoverView: View {
    let items = ["Row 1", "Row 2", "Row 3", "Row 4", "Row 5"]

    
    var body: some View {
        List(items, id: \.self) { item in
            ListRowView(item: item)
        }
    }
}

struct ListRowView: View {
    let item: String
    @State private var showPopover = false
    
    var body: some View {
        HStack {
            Text(item)
                .padding()
            Spacer()
        }
        .contentShape(Rectangle()) // Ensures the entire row is tappable
        .onTapGesture {
            showPopover = true
        }
        .popover(isPresented: $showPopover, attachmentAnchor: .rect(.bounds), arrowEdge: .bottom) {
            PopoverContentView(item: item)
        }
    }
}

struct PopoverContentView: View {
    let item: String

    var body: some View {
        VStack {
            Text("Selected: \(item)")
                .padding()
        }
        .frame(width: 200, height: 100)
    }
}


#Preview {
    TestPopoverView()
        .frame(width: 300, height: 350)
}

How do I get the mouse location and the popover showing from that precise location? I don't understand how to specify the attachmentAnchor to do this, while working inside a List.

Share Improve this question edited Mar 30 at 0:09 soundflix 2,86312 gold badges16 silver badges34 bronze badges asked Mar 27 at 19:47 Z SZ S 7,53312 gold badges58 silver badges110 bronze badges 1
  • Did my answer help solving your issue? – soundflix Commented 2 days ago
Add a comment  | 

1 Answer 1

Reset to default 0

Unfortunately, I don't know if there is an easy answer.
But it's makeable and the flow is like this:

  1. PopoverAttachmentAnchor takes either a rect or a point. The type of this point is UnitPoint.

A normalized 2D point in a view’s coordinate space.

  1. This means we need the location where the mouse clicks and the size of the view.

  2. The size we can get with GeometryReader, I use a custom modifier called readSize to hide the details from the view's body.

  3. The local coordinates of the mouse pointer we get with the onContinuousHover modifier.

  4. We store both values as they change.

  5. Finally, when the mouse clicks, we calculate the current click point in UnitPoints to use it in the popover modifier.

This is the full code:

import SwiftUI

struct TestPopoverView: View {
    let items = ["Row 1", "Row 2", "Row 3", "Row 4", "Row 5"]
    
    var body: some View {
        List(items, id: \.self) { item in
            ListRowView(item: item)
        }
    }
}

struct ListRowView: View {
    let item: String
    @State private var showPopover = false
    @State private var size: CGSize = .zero
    @State private var mouseLocation: CGPoint?

    func normalize(point: CGPoint?, in size: CGSize)-> UnitPoint? {
        guard let point else {
            return nil
        }
        return UnitPoint(x: point.x / size.width, y: point.y / size.height)
    }
    
    @State var clickPoint: UnitPoint = .center
    
    var body: some View {
        HStack {
            Text(item)
                .padding()
            Spacer()
        }
        .border(Color.yellow)
        .contentShape(Rectangle()) // Ensures the entire row is tappable
        
        .readSize { size in
            self.size = size
        }

        .onContinuousHover { phase in
            switch phase {
            case .active(let location):
                mouseLocation = location
            case .ended:
                break
            }
        }
        
        .onTapGesture {
            if let point = normalize(point: mouseLocation, in: size) {
                clickPoint = point
                showPopover = true
            }
        }
        .popover(isPresented: $showPopover, attachmentAnchor: .point(clickPoint), arrowEdge: .bottom) {
            PopoverContentView(item: item)
        }
    }
}

extension View {
    func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
        background {
            GeometryReader { geometryProxy in
                Color.clear
                    .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
            }
        }
        .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
    }
}

private struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

If you are targeting macOS 13+, you could replace readSize with onGeometryChange.

        .onGeometryChange(for: CGSize.self) { proxy in
            proxy.size
        } action: {
            self.size = $0
        }

本文标签: macosSwiftUI Show popover at mouse click locationStack Overflow