如何在自定义布局中查找子视图位置,判断离鼠标最近的App图标
问题描述
我实现了一个带有自定义布局WheelLayout的View,运行效果良好,简化代码如下:
struct SomeContent: View { ... var body: some View { WheelLayout { ForEach(targets, id: \.self) { app in VStack { app.icon != nil ? Image(nsImage: app.icon!) : Image(systemName: "rectangle.dashed") Text(app.name).bold() } } } } }
现在我想知道哪个app图标离鼠标光标最近。我认为解决方案的一部分是将上述VStack替换为持有app引用的自定义View类,但在包含SomeContent的窗口代码中,如何查询该类的所有子视图以检查它们的位置?
解决方案
1. 用自定义View封装并自动上报位置
先把每个app的视图封装成自定义View,通过GeometryReader获取自身在全局坐标系的位置和尺寸,同时监听鼠标位置计算距离:
struct AppItemView: View { let app: AppModel // 替换为你的实际app数据模型 @Binding var nearestApp: AppModel? @Binding var nearestDistance: CGFloat @State private var globalFrame: CGRect = .zero var body: some View { VStack { app.icon != nil ? Image(nsImage: app.icon!) : Image(systemName: "rectangle.dashed") Text(app.name).bold() } .background( GeometryReader { geo in Color.clear .onAppear { globalFrame = geo.frame(in: .global) } .onChange(of: geo.frame(in: .global)) { globalFrame = $0 } } ) .onReceive(MouseMonitor.shared.positionPublisher) { mousePos in let currentDistance = hypot(mousePos.x - globalFrame.midX, mousePos.y - globalFrame.midY) if currentDistance < nearestDistance { nearestDistance = currentDistance nearestApp = app } } } }
2. 全局监听鼠标位置
实现一个全局鼠标位置监听工具,通过NSEvent捕获鼠标移动事件:
class MouseMonitor { static let shared = MouseMonitor() let positionPublisher = PassthroughSubject<CGPoint, Never>() private init() { NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { [weak self] event in self?.positionPublisher.send(event.locationInWindow) return event } } }
在App启动时自动初始化监听(比如在App结构体的init里调用_ = MouseMonitor.shared)。
3. 父视图管理全局状态
修改SomeContent,维护最近app和对应距离的状态,传递给每个子视图:
struct SomeContent: View { let targets: [AppModel] @State private var nearestApp: AppModel? @State private var nearestDistance: CGFloat = .greatestFiniteMagnitude var body: some View { WheelLayout { ForEach(targets, id: \.self) { app in AppItemView( app: app, nearestApp: $nearestApp, nearestDistance: $nearestDistance ) } } } }
4. 更高效的方案:在自定义布局中直接计算
如果你的WheelLayout是自定义Layout协议实现,可以在布局过程中直接记录每个子视图的位置,统一计算最近app,避免每个子视图单独监听:
struct WheelLayout: Layout { let targets: [AppModel] @Binding var nearestApp: AppModel? @Binding var mousePos: CGPoint private var itemFrames: [AppModel: CGRect] = [:] func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { // 你的原有布局尺寸计算逻辑 } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { itemFrames.removeAll() // 遍历子视图并布局,同时记录每个app对应的全局frame for (index, subview) in subviews.enumerated() { let app = targets[index] let itemFrame = // 计算当前子视图的全局位置和尺寸 subview.place(at: itemFrame.origin, proposal: proposal) itemFrames[app] = itemFrame } // 布局完成后计算最近的app updateNearestApp() } private func updateNearestApp() { var closestApp: AppModel? var minDistance: CGFloat = .greatestFiniteMagnitude for (app, frame) in itemFrames { let distance = hypot(mousePos.x - frame.midX, mousePos.y - frame.midY) if distance < minDistance { minDistance = distance closestApp = app } } nearestApp = closestApp } }
内容的提问来源于stack exchange,提问作者viraptor
相关产品推荐
相关产品推荐

