Mac Catalyst: 光标位置与 UIView 上的触摸位置不匹配

huangapple go评论65阅读模式
英文:

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

  1. Turn your cursor into a crosshair icon upon hovering
  2. Draw a circle centered at the cursor upon clicking

这只做了两件事:

  1. 在悬停时将光标变成十字标志
  2. 单击时在光标处绘制一个圆圈

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

  1. Turn your cursor into a crosshair icon upon hovering
  2. 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&#39;s `hotSpot` is at the tip of the **black** arrow -- *not* the white outline.

With the `.crosshair` pointer, it *appears* the cursor&#39;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&lt;UITouch&gt;, 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&#39;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&lt;UITouch&gt;, 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>



huangapple
  • 本文由 发表于 2023年6月1日 12:58:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/76378776.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定