最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

macos - SwiftUI: Show popover at mouse click location - Stack Overflow

programmeradmin1浏览0评论

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
        }
发布评论

评论列表(0)

  1. 暂无评论