在macOS中如何用Swift检测点击的是哪个CAShapeLayer?
点击选中并修改CAShapeLayer的问题解决建议
我有一个包含多个CAShapeLayer对象(也可能存在其他CALayer对象)的视图,想要修改用户点击的CAShapeLayer对象。尝试了以下两种方法但均未生效,传递给函数的point参数是NSEvent.locationInWindow,不确定是否需要针对CAShapeLayer进行坐标转换。
尝试过的方法
方法一
private func modifyDrawing(at point: NSPoint) { for layer in view.layer!.sublayers! { let s = layer.hitTest(point) if s != nil && s is CAShapeLayer { selectedShape = s as? CAShapeLayer } } // modify some properties selectedShape?.shadowRadius = 20 selectedShape?.shadowOpacity = 1 selectedShape?.shadowColor = CGColor.black }
方法二
private func modifyDrawing(at point: NSPoint) { let drawingsAtMouseClick: [CAShapeLayer] = view.layer!.sublayers!.compactMap{ $0 as? CAShapeLayer } if drawingsAtMouseClick.isEmpty { return } for drawing in drawingsAtMouseClick { if drawing.contains(point) { selectedShape = drawing break } } // modify some properties selectedShape?.shadowRadius = 20 selectedShape?.shadowOpacity = 1 selectedShape?.shadowColor = CGColor.black }
问题核心
两种方法失效的关键原因是坐标系统不匹配:NSEvent.locationInWindow是窗口坐标系下的点,而CALayer的hitTest(_:)和contains(_:)方法需要的是该layer自身本地坐标系下的点,直接传递窗口坐标会导致坐标错位,无法正确命中目标CAShapeLayer。
修正方案
需要把窗口坐标经过两次转换,拿到对应layer的本地坐标后再做命中检测:
- 先将窗口坐标转换为视图坐标系的点
- 再将视图坐标转换为目标layer的本地坐标
改进后的方法一(基于hitTest)
private func modifyDrawing(at windowPoint: NSPoint) { // 转换窗口坐标到视图坐标 let viewPoint = view.convert(windowPoint, from: nil) for layer in view.layer!.sublayers! { // 转换视图坐标到当前layer的本地坐标 let layerPoint = layer.convert(viewPoint, from: view.layer) if let hitLayer = layer.hitTest(layerPoint) as? CAShapeLayer { selectedShape = hitLayer break // 找到第一个命中的layer就停止遍历 } } // 修改选中layer的属性 selectedShape?.shadowRadius = 20 selectedShape?.shadowOpacity = 1 selectedShape?.shadowColor = CGColor.black }
改进后的方法二(基于路径精确命中)
如果需要精确命中CAShapeLayer的路径(而不是仅仅命中layer的bounds范围),可以用路径的contains(_:)方法判断:
private func modifyDrawing(at windowPoint: NSPoint) { let viewPoint = view.convert(windowPoint, from: nil) let shapeLayers = view.layer!.sublayers!.compactMap { $0 as? CAShapeLayer } guard !shapeLayers.isEmpty else { return } for shapeLayer in shapeLayers { let layerPoint = shapeLayer.convert(viewPoint, from: view.layer) // 优先判断路径是否包含点,再判断bounds(兼容路径超出bounds的情况) if shapeLayer.path?.contains(layerPoint) == true || shapeLayer.contains(layerPoint) { selectedShape = shapeLayer break } } // 修改选中layer的属性 selectedShape?.shadowRadius = 20 selectedShape?.shadowOpacity = 1 selectedShape?.shadowColor = CGColor.black }
额外说明
hitTest(_:)会自动考虑layer的isHidden、opacity(低于0.01时不会被命中)、mask等属性,是更全面的命中检测方式。- 如果你的CAShapeLayer设置了
transform(比如缩放、旋转),坐标转换也会自动适配,不需要额外处理。
内容的提问来源于stack exchange,提问作者Ram Patra
相关产品推荐
相关产品推荐

