英文:
Mac Catalyst: Cursor position doesn't match touch location on UIView
问题
I'm trying to build a drawing tool on Xcode building for Mac Catalyst, using the UIKit Framework, but I'm running into issues with cursor location accuracy:
在尝试使用UIKit框架在Xcode上构建适用于Mac Catalyst的绘图工具时,我遇到了光标位置准确性的问题:
After initializing an iOS project configured for Swift and Storyboard, here's an absolutely minimal ViewController.swift that produces the bug:
在初始化了一个配置为Swift和Storyboard的iOS项目之后,这是一个完全最小化的ViewController.swift,会出现这个bug:
import UIKit
class CanvasView: UIView {
var drawPoint: CGPoint = .zero
let radius = 12.0
override func draw(_: CGRect) {
UIGraphicsGetCurrentContext()!
.fillEllipse(in: CGRect(x: drawPoint.x - radius,
y: drawPoint.y - radius,
width: radius * 2,
height: radius * 2))
}
override func touchesBegan(_ touches: Set<UITouch>, with _: UIEvent?) {
drawPoint = touches.first!.location(in: self)
setNeedsDisplay()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view = CanvasView()
view.backgroundColor = .white
let hover = UIHoverGestureRecognizer(target: self, action: #selector(hover))
view.addGestureRecognizer(hover)
}
@objc func hover(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began, .changed: NSCursor.crosshair.set()
default: NSCursor.arrow.set()
}
}
}
All this does is
- Turn your cursor into a crosshair icon upon hovering
- Draw a circle centered at the cursor upon clicking
这只做了两件事:
- 在悬停时将光标变成十字标志
- 单击时在光标处绘制一个圆圈
True enough, this draws a circle centered at the cursor but it's always off by a pixel or two, and moreover this inaccuracy is inconsistent.
的确,这会在光标处绘制一个圆圈,但它总是偏离一个或两个像素,而且这种不准确性是不一致的。
Sometimes the circle drawn in offset by 1 pixel to the right and 1 pixel down, sometimes it's 2, and this is a major issue when it comes to users trying to draw things precisely.
有时,绘制的圆圈向右偏移1个像素,向下偏移1个像素,有时候是2个像素,这在用户试图精确绘制东西时是一个重大问题。
I've tried using CAShapeLayer
and its corresponding func draw(_: CALayer, in: CGContext)
, but this exact same inaccuracy is reproduced there.
我尝试使用CAShapeLayer
及其对应的func draw(_: CALayer, in: CGContext)
,但在那里也重现了完全相同的不准确性。
I've also tried using preciseLocation(in:)
instead of location(in:)
but again the bug is still there.
我还尝试使用preciseLocation(in:)
代替location(in:)
,但问题仍然存在。
Notably, when I tried building for iPhone/iPad and Xcode opens the same code in a Simulator, this bug vanishes, and the circle is centered perfectly on the touch point.
值得注意的是,当我尝试为iPhone/iPad构建并且Xcode在模拟器中打开相同的代码时,这个bug消失了,圆圈完美地居中在触摸点上。
Any help is appreciated!
任何帮助都将不胜感激!
英文:
I'm trying to build a drawing tool on Xcode building for Mac Catalyst, using the UIKit Framework, but I'm running into issues with cursor location accuracy:
After initializing an iOS project configured for Swift and Storyboard, here's an absolutely minimal ViewController.swift that produces the bug:
import UIKit
class CanvasView: UIView {
var drawPoint: CGPoint = .zero
let radius = 12.0
override func draw(_: CGRect) {
UIGraphicsGetCurrentContext()!
.fillEllipse(in: CGRect(x: drawPoint.x - radius,
y: drawPoint.y - radius,
width: radius * 2,
height: radius * 2))
}
override func touchesBegan(_ touches: Set<UITouch>, with _: UIEvent?) {
drawPoint = touches.first!.location(in: self)
setNeedsDisplay()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view = CanvasView()
view.backgroundColor = .white
let hover = UIHoverGestureRecognizer(target: self, action: #selector(hover))
view.addGestureRecognizer(hover)
}
@objc func hover(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began, .changed: NSCursor.crosshair.set()
default: NSCursor.arrow.set()
}
}
}
All this does is
- Turn your cursor into a crosshair icon upon hovering
- Draw a circle centered at the cursor upon clicking
True enough, this draws a circle centered at the cursor but it's always off by a pixel or two, and moreover this inaccuracy is inconsistent.
Sometimes the circle drawn in offset by 1 pixel to the right and 1 pixel down, sometimes it's 2, and this is a major issue when it comes to users trying to draw things precisely.
I've tried using CAShapeLayer
and its corresponding func draw(_: CALayer, in: CGContext)
, but this exact same inaccuracy is reproduced there.
I've also tried using preciseLocation(in:)
instead of location(in:)
but again the bug is still there.
Notably, when I tried building for iPhone/iPad and Xcode opens the same code in a Simulator, this bug vanishes, and the circle is centered perfectly on the touch point.
Any help is appreciated!
EDIT:
This time I tried using a minimal working example with Cocoa
only, no Mac Catalyst nor UIKit, and somehow, the problem still persists. Here's the ViewController.swift code:
import Cocoa
class View: NSView {
var point: CGPoint = .zero
let radius: CGFloat = 20
override func draw(_: NSRect) {
NSColor.blue.setStroke()
let cross = NSBezierPath()
let v = radius * 2
cross.move(to: CGPoint(x: point.x - v, y: point.y))
cross.line(to: CGPoint(x: point.x + v, y: point.y))
cross.move(to: CGPoint(x: point.x, y: point.y - v))
cross.line(to: CGPoint(x: point.x, y: point.y + v))
cross.lineWidth = 0.8
cross.stroke()
}
override func mouseDown(with event: NSEvent) {
point = event.locationInWindow
setNeedsDisplay(bounds)
NSCursor.crosshair.set()
}
}
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let sub = View(frame: view.frame)
sub.autoresizingMask = [.height, .width]
view.addSubview(sub)
}
}
Only this time I use a cross to show more clearly that the alignment is off.
I thought this might be a result of the build target being Debug, but this bug still shows up in an Archive built for Release.
答案1
得分: 0
macOS的鼠标指针有点古怪...
默认的箭头指针似乎光标的hotSpot
位于黑色箭头的尖端,而不是白色轮廓。
使用.crosshair
指针时,光标的hotSpot
似乎略微偏右下实际的交叉点。
为了获得精度,您可能希望尝试自定义光标图像。
快速示例:
import UIKit
class CanvasView: UIView {
var drawPoint: CGPoint = .zero
let radius = 12.0
override func draw(_: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// 绘制一个100x100的矩形,以便我们有一些角点可以单击
ctx.stroke(.init(x: 100.0, y: 100.0, width: 100.0, height: 100.0))
// 半透明的蓝色矩形,顶部左上角位于触摸点
ctx.setFillColor(UIColor.blue.withAlphaComponent(0.75).cgColor)
ctx.fill([.init(origin: drawPoint, size: .init(width: radius, height: radius))])
// 半透明的红色椭圆,以触摸点为中心
ctx.setFillColor(UIColor.red.withAlphaComponent(0.5).cgColor)
ctx.fillEllipse(in: CGRect(x: drawPoint.x - radius,
y: drawPoint.y - radius,
width: radius * 2,
height: radius * 2))
}
override func touchesBegan(_ touches: Set<UITouch>, with _: UIEvent?) {
drawPoint = touches.first!.location(in: self)
// 可能需要四舍五入点?或floor()或ceil()?
// drawPoint.x = round(drawPoint.x)
// drawPoint.y = round(drawPoint.y)
setNeedsDisplay()
}
}
class ViewController: UIViewController {
// 我们将使用自定义光标
var c: NSCursor!
override func viewDidLoad() {
super.viewDidLoad()
view = CanvasView()
view.backgroundColor = .white
let hover = UIHoverGestureRecognizer(target: self, action: #selector(hover))
view.addGestureRecognizer(hover)
// 创建自定义十字光标图像
let r: CGRect = .init(origin: .zero, size: .init(width: 20.0, height: 20.0))
let rn = UIGraphicsImageRenderer(size: r.size)
let img = rn.image { _ in
let bez = UIBezierPath()
bez.move(to: .init(x: r.minX, y: r.midY))
bez.addLine(to: .init(x: r.maxX, y: r.midY))
bez.move(to: .init(x: r.midX, y: r.minY))
bez.addLine(to: .init(x: r.midX, y: r.maxY))
bez.stroke()
}
// 设置自定义光标
c = NSCursor(image: img, hotSpot: .init(x: r.midX - 0.5, y: r.midY - 0.5))
}
@objc func hover(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
c.set()
default:
()
}
}
}
注意:这只是示例代码。
编辑
我还尝试了另一种选择 - 隐藏光标并在CanvasView
中绘制十字线:
class CanvasView: UIView {
var cursorPoint: CGPoint = .zero
var drawPoint: CGPoint = .zero
let radius = 12.0
override func draw(_: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// 绘制一个100x100的矩形,以便我们有一些角点可以单击
ctx.stroke(.init(x: 100.0, y: 100.0, width: 100.0, height: 100.0))
// 半透明的蓝色矩形,顶部左上角位于触摸点
ctx.setFillColor(UIColor.blue.withAlphaComponent(0.75).cgColor)
ctx.fill([.init(origin: drawPoint, size: .init(width: radius, height: radius))])
// 半透明的红色椭圆,以触摸点为中心
ctx.setFillColor(UIColor.red.withAlphaComponent(0.5).cgColor)
ctx.fillEllipse(in: CGRect(x: drawPoint.x - radius,
y: drawPoint.y - radius,
width: radius * 2,
height: radius * 2))
let r: CGRect = CGRect(x: cursorPoint.x - radius,
y: cursorPoint.y - radius,
width: radius * 2,
height: radius * 2)
let bez = UIBezierPath()
bez.move(to: .init(x: r.minX, y: r.midY))
bez.addLine(to: .init(x: r.maxX, y: r.midY))
bez.move(to: .init(x: r.midX, y: r.minY))
bez.addLine(to: .init(x: r.midX, y: r.maxY))
ctx.setFillColor(UIColor.black.cgColor)
bez.stroke()
}
override func touchesBegan(_ touches: Set<UITouch>, with _: UIEvent?) {
drawPoint = touches.first!.location(in: self)
// 可能需要四舍五入点?或floor()或ceil()?
drawPoint.x = round(drawPoint.x)
drawPoint.y = round(drawPoint.y)
setNeedsDisplay()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view = CanvasView()
view.backgroundColor = .white
let hover = UIHoverGestureRecognizer(target: self, action: #selector(hover))
view.addGestureRecognizer(hover)
}
@objc func hover(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began:
// 隐藏光标...
// 我们将在CanvasView中绘制自己的十字线
NSCursor.hide()
case .changed:
if let v = view as? CanvasView {
var p = recognizer.location(in: v)
p.x = round(p.x)
p.y = round(p.y)
v.cursorPoint = p
v.setNeedsDisplay()
}
default:
if let v = view as? CanvasView
<details>
<summary>英文:</summary>
The MacOS mouse pointers are a little quirky...
With the default Arrow pointer, it *appears* the cursor's `hotSpot` is at the tip of the **black** arrow -- *not* the white outline.
With the `.crosshair` pointer, it *appears* the cursor's `hotSpot` is a little to the right and below the actual cross-point.
To get precision, you may want to experiment with a custom cursor image.
Quick example:
import UIKit
class CanvasView: UIView {
var drawPoint: CGPoint = .zero
let radius = 12.0
override func draw(_: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// stroke a 100x100 rectangle so we have some corner-points to click on
ctx.stroke(.init(x: 100.0, y: 100.0, width: 100.0, height: 100.0))
// translucent blue rectangle, with top-left corner at touch-point
ctx.setFillColor(UIColor.blue.withAlphaComponent(0.75).cgColor)
ctx.fill([.init(origin: drawPoint, size: .init(width: radius, height: radius))])
// translucent red ellipse with center at touch-point
ctx.setFillColor(UIColor.red.withAlphaComponent(0.5).cgColor)
ctx.fillEllipse(in: CGRect(x: drawPoint.x - radius,
y: drawPoint.y - radius,
width: radius * 2,
height: radius * 2))
}
override func touchesBegan(_ touches: Set<UITouch>, with _: UIEvent?) {
drawPoint = touches.first!.location(in: self)
// might want to round the point? or floor() or ceil() ?
//drawPoint.x = round(drawPoint.x)
//drawPoint.y = round(drawPoint.y)
setNeedsDisplay()
}
}
class ViewController: UIViewController {
// we'll use a custom cursor
var c: NSCursor!
override func viewDidLoad() {
super.viewDidLoad()
view = CanvasView()
view.backgroundColor = .white
let hover = UIHoverGestureRecognizer(target: self, action: #selector(hover))
view.addGestureRecognizer(hover)
// create a custom cross-hairs cursor image
let r: CGRect = .init(origin: .zero, size: .init(width: 20.0, height: 20.0))
let rn = UIGraphicsImageRenderer(size: r.size)
let img = rn.image { _ in
let bez = UIBezierPath()
bez.move(to: .init(x: r.minX, y: r.midY))
bez.addLine(to: .init(x: r.maxX, y: r.midY))
bez.move(to: .init(x: r.midX, y: r.minY))
bez.addLine(to: .init(x: r.midX, y: r.maxY))
bez.stroke()
}
// set the custom cursor
c = NSCursor(image: img, hotSpot: .init(x: r.midX - 0.5, y: r.midY - 0.5))
}
@objc func hover(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
c.set()
default:
()
}
}
}
Note: This is **Example Code Only**
------------
**Edit**
Another option I was playing around with -- **hide** the cursor and draw a cross-hairs in `CanvasView`:
class CanvasView: UIView {
var cursorPoint: CGPoint = .zero
var drawPoint: CGPoint = .zero
let radius = 12.0
override func draw(_: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
// stroke a 100x100 rectangle so we have some corner-points to click on
ctx.stroke(.init(x: 100.0, y: 100.0, width: 100.0, height: 100.0))
// translucent blue rectangle, with top-left corner at touch-point
ctx.setFillColor(UIColor.blue.withAlphaComponent(0.75).cgColor)
ctx.fill([.init(origin: drawPoint, size: .init(width: radius, height: radius))])
// translucent red ellipse with center at touch-point
ctx.setFillColor(UIColor.red.withAlphaComponent(0.5).cgColor)
ctx.fillEllipse(in: CGRect(x: drawPoint.x - radius,
y: drawPoint.y - radius,
width: radius * 2,
height: radius * 2))
let r: CGRect = CGRect(x: cursorPoint.x - radius,
y: cursorPoint.y - radius,
width: radius * 2,
height: radius * 2)
let bez = UIBezierPath()
bez.move(to: .init(x: r.minX, y: r.midY))
bez.addLine(to: .init(x: r.maxX, y: r.midY))
bez.move(to: .init(x: r.midX, y: r.minY))
bez.addLine(to: .init(x: r.midX, y: r.maxY))
ctx.setFillColor(UIColor.black.cgColor)
bez.stroke()
}
override func touchesBegan(_ touches: Set<UITouch>, with _: UIEvent?) {
drawPoint = touches.first!.location(in: self)
// might want to round the point? or floor() or ceil() ?
drawPoint.x = round(drawPoint.x)
drawPoint.y = round(drawPoint.y)
setNeedsDisplay()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view = CanvasView()
view.backgroundColor = .white
let hover = UIHoverGestureRecognizer(target: self, action: #selector(hover))
view.addGestureRecognizer(hover)
}
@objc func hover(_ recognizer: UIHoverGestureRecognizer) {
switch recognizer.state {
case .began:
// hide the cursor...
// we will draw our own cross-hairs in CanvasView
NSCursor.hide()
case .changed:
if let v = view as? CanvasView {
var p = recognizer.location(in: v)
p.x = round(p.x)
p.y = round(p.y)
v.cursorPoint = p
v.setNeedsDisplay()
}
default:
if let v = view as? CanvasView {
v.cursorPoint = .zero
v.setNeedsDisplay()
}
// un-hide the cursor when mouse leaves
NSCursor.unhide()
}
}
}
As before, this is ***Example Code Only!!!*** -- but it may be worth a look :)
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论