SwiftUI:检测 Mac 触控板上的手指位置 [英] SwiftUI: Detect finger position on Mac trackpad
问题描述
我正在为 macOS 制作一个 SwiftUI 应用程序,我想通过检测用户手指的位置将触控板用作 (x, y) 输入.我希望能够检测到多个手指搁置在触控板上(而不是拖动).我该怎么做?
第 1 步 - AppKit
- SwiftUI 未提供所有必需的信息
- AppKit &
NSTouch
确实 -normalizedPosition
.
第一步是创建一个简单的 AppKitTouchesView
通过委托转发所需的触摸.
import SwiftUI导入应用套件协议 AppKitTouchesViewDelegate: AnyObject {//仅提供 `.touching` 触摸.func touchesView(_ view: AppKitTouchesView, didUpdateTouchingTouches touches: Set<NSTouch>)}最后一类 AppKitTouchesView: NSView {弱变量委托:AppKitTouchesViewDelegate?覆盖初始化(框架frameRect:NSRect){super.init(框架:frameRect)//我们只对 `.indirect` 接触感兴趣.allowedTouchTypes = [.indirect]//我们也想接收休息的触摸.想要RestingTouches = true}需要初始化?(编码器:NSCoder){fatalError("init(coder:) 尚未实现")}私有 func handleTouches(带事件:NSEvent){//仅获取所有`.touching` 触摸(包括`.began`、`.moved` 和`.stationary`).让 touches = event.touches(matching: .touching, in: self)//通过委托转发它们.委托?.touchesView(self, didUpdateTouchingTouches: touches)}覆盖 func touchesBegan(with event: NSEvent) {handleTouches(带:事件)}覆盖 func touchesEnded(with event: NSEvent) {handleTouches(带:事件)}覆盖 func touchesMoved(with event: NSEvent) {handleTouches(带:事件)}覆盖 func touchesCancelled(with event: NSEvent) {handleTouches(带:事件)}}
第 2 步 - 简化触控结构
第二步是创建一个简单的自定义 Touch
结构,该结构仅包含所有必需的信息并且与 SwiftUI 兼容(不翻转 y
).
struct Touch: Identifiable {//`可识别` ->`ForEach` 需要 `id`(见下文).让 ID:Int//设备上标准化的触摸 X 位置(0.0 - 1.0).让 normalizedX: CGFloat//设备上标准化的触摸 Y 位置(0.0 - 1.0).让 normalizedY: CGFloat初始化(_ nsTouch:NSTouch){self.normalizedX = nsTouch.normalizedPosition.x//`NSTouch.normalizedPosition.y` 被翻转 ->0.0 表示底部.但该//`Touch` 结构旨在与 SwiftUI 一起使用 ->翻转.self.normalizedY = 1.0 - nsTouch.normalizedPosition.yself.id = nsTouch.hash}}
第 3 步 - 为 SwiftUI 包装它
NSViewRepresentable
文档绑定
文档
第三步是创建一个包含我们的 AppKit AppKitTouchesView
视图的 SwiftUI 视图.
struct TouchesView: NSViewRepresentable {//最新的触摸列表.@Binding var 触摸:[触摸]func updateNSView(_ nsView: AppKitTouchesView, context: Context) {}func makeNSView(context: Context) ->AppKitTouchesView {让视图 = AppKitTouchesView()view.delegate = context.coordinator返回视图}func makeCoordinator() ->协调员{协调员(自己)}类协调器:NSObject,AppKitTouchesViewDelegate {让父母:TouchesView初始化(_ 视图:TouchesView){self.parent = 视图}func touchesView(_ view: AppKitTouchesView, didUpdateTouchingTouches touches: Set<NSTouch>) {parent.touches = touches.map(Touch.init)}}}
第 4 步 - 制作 TrackPadView
第四步是创建一个 TrackPadView
,它在内部确实使用了我们的TouchesView
并在其上绘制代表手指物理位置的圆圈.
struct TrackPadView: View {私人让 touchViewSize: CGFloat = 20@State var touches: [Touch] = []var主体:一些视图{ZStack {GeometryReader { 代理输入TouchesView(touches: self.$touches)ForEach(self.touches) { 触摸圆圈().foregroundColor(Color.green).frame(宽度:self.touchViewSize,高度:self.touchViewSize).抵消(x: proxy.size.width * touch.normalizedX - self.touchViewSize/2.0,y:proxy.size.height * touch.normalizedY - self.touchViewSize/2.0)}}}}}
步骤 5 - 在主 ContentView
中使用它第五步是在我们的主视图中使用它,使用一些接近真实触控板纵横比的纵横比.
struct ContentView: View {var主体:一些视图{TrackPadView().background(颜色.灰色).aspectRatio(1.6, contentMode: .fit).填充().frame(maxWidth: .infinity, maxHeight: .infinity)}}
完成项目
- 打开 Xcode
- 创建一个新项目(macOS App & Swift & SwiftUI)
- 复制&从这个要点 粘贴
ContentView.swift
I'm making a SwiftUI app for macOS and I'd like to use the trackpad as an (x, y) input by detecting the position of the user's fingers. I wanna be able to detect multiple fingers that are resting on the trackpad (not dragging). How do I do that?
A similar question has been asked before, but I'm asking again because that was from nearly 10 years ago, the answers are all in Obj-C (one in Swift 3), and I'm wondering if there's an updated methodology. Most importantly, I've no clue how to implement the Obj-C code into my SwiftUI app so if there isn't any updated methodology, I'd appreciate if someone could just explain how to implement the old Obj-C code.
To demonstrate what I mean, this video demo of the AudioSwift app does exactly what I want. macOS itself also uses this for hand-writing Chinese (although I don't need to recognize characters).
Always split your task into smaller ones and do them one by one. Ask in the same way and avoid broad questions touching lot of topics.
Goal
- Track pad view (gray rectangle)
- Circles on top of it showing fingers physical position
Step 1 - AppKit
- SwiftUI doesn't provide all the required information
- AppKit &
NSTouch
does -normalizedPosition
.
First step is to create a simple AppKitTouchesView
forwarding required touches via a delegate.
import SwiftUI
import AppKit
protocol AppKitTouchesViewDelegate: AnyObject {
// Provides `.touching` touches only.
func touchesView(_ view: AppKitTouchesView, didUpdateTouchingTouches touches: Set<NSTouch>)
}
final class AppKitTouchesView: NSView {
weak var delegate: AppKitTouchesViewDelegate?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
// We're interested in `.indirect` touches only.
allowedTouchTypes = [.indirect]
// We'd like to receive resting touches as well.
wantsRestingTouches = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func handleTouches(with event: NSEvent) {
// Get all `.touching` touches only (includes `.began`, `.moved` & `.stationary`).
let touches = event.touches(matching: .touching, in: self)
// Forward them via delegate.
delegate?.touchesView(self, didUpdateTouchingTouches: touches)
}
override func touchesBegan(with event: NSEvent) {
handleTouches(with: event)
}
override func touchesEnded(with event: NSEvent) {
handleTouches(with: event)
}
override func touchesMoved(with event: NSEvent) {
handleTouches(with: event)
}
override func touchesCancelled(with event: NSEvent) {
handleTouches(with: event)
}
}
Step 2 - Simplified touch structure
Second step is to create a simple custom Touch
structure which holds all the required information only and is SwiftUI compatible (not flipped y
).
struct Touch: Identifiable {
// `Identifiable` -> `id` is required for `ForEach` (see below).
let id: Int
// Normalized touch X position on a device (0.0 - 1.0).
let normalizedX: CGFloat
// Normalized touch Y position on a device (0.0 - 1.0).
let normalizedY: CGFloat
init(_ nsTouch: NSTouch) {
self.normalizedX = nsTouch.normalizedPosition.x
// `NSTouch.normalizedPosition.y` is flipped -> 0.0 means bottom. But the
// `Touch` structure is meants to be used with the SwiftUI -> flip it.
self.normalizedY = 1.0 - nsTouch.normalizedPosition.y
self.id = nsTouch.hash
}
}
Step 3 - Wrap it for the SwiftUI
NSViewRepresentable
documentationBinding
documentation
Third step is to create a SwiftUI view wrapping our AppKit AppKitTouchesView
view.
struct TouchesView: NSViewRepresentable {
// Up to date list of touching touches.
@Binding var touches: [Touch]
func updateNSView(_ nsView: AppKitTouchesView, context: Context) {
}
func makeNSView(context: Context) -> AppKitTouchesView {
let view = AppKitTouchesView()
view.delegate = context.coordinator
return view
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, AppKitTouchesViewDelegate {
let parent: TouchesView
init(_ view: TouchesView) {
self.parent = view
}
func touchesView(_ view: AppKitTouchesView, didUpdateTouchingTouches touches: Set<NSTouch>) {
parent.touches = touches.map(Touch.init)
}
}
}
Step 4 - Make a TrackPadView
Fourth step is to create a TrackPadView
which internally does use our
TouchesView
and draws circles on it representing physical location of fingers.
struct TrackPadView: View {
private let touchViewSize: CGFloat = 20
@State var touches: [Touch] = []
var body: some View {
ZStack {
GeometryReader { proxy in
TouchesView(touches: self.$touches)
ForEach(self.touches) { touch in
Circle()
.foregroundColor(Color.green)
.frame(width: self.touchViewSize, height: self.touchViewSize)
.offset(
x: proxy.size.width * touch.normalizedX - self.touchViewSize / 2.0,
y: proxy.size.height * touch.normalizedY - self.touchViewSize / 2.0
)
}
}
}
}
}
Step 5 - Use it in the main ContentView
Fifth step is to use it in our main view with some aspect ratio which is close to the real trackpad aspect ratio.
struct ContentView: View {
var body: some View {
TrackPadView()
.background(Color.gray)
.aspectRatio(1.6, contentMode: .fit)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Complete project
- Open Xcode
- Create a new project (macOS App & Swift & SwiftUI)
- Copy & paste
ContentView.swift
from this gist
这篇关于SwiftUI:检测 Mac 触控板上的手指位置的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!