SwiftUI:检测 Mac 触控板上的手指位置 [英] SwiftUI: Detect finger position on Mac trackpad

查看:23
本文介绍了SwiftUI:检测 Mac 触控板上的手指位置的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在为 macOS 制作一个 SwiftUI 应用程序,我想通过检测用户手指的位置将触控板用作 (x, y) 输入.我希望能够检测到多个手指搁置在触控板上(而不是拖动).我该怎么做?

第 1 步 - AppKit

第一步是创建一个简单的 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 包装它

第三步是创建一个包含我们的 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

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

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆