在 SwiftUI 中检测 Siri 远程滑动

Detect Siri Remote swipe in SwiftUI

如何从 SwiftUI 识别 Siri Remote 滑动手势。

我好像还没有实现,那我该如何解决呢?

这个问题我有2个答案,我会分别回答,让你们决定哪个是最好的,

首先是 apple 方式(这显然并不总是有效,捕获的点击次数多于滑动次数):

import SwiftUI

struct SwipeTestView: View
{
    var body: some View
    {
        Text("This can be some full screen image or what not")
            .focusable() // <-- this is a must
            .onMoveCommand { direction in  // <-- this $#!* can't tell a move swipe from a touch (direction is of type: MoveCommandDirection)
                print("Direction: \(direction)")
                if direction == .left { print(">>> left swipe detected") }
                if direction == .right { print(">>> right swipe detected") }
                if direction == .up { print(">>> up swipe detected") }
                if direction == .down { print(">>> down swipe detected") }
            }

    }
}

真的(我怎么强调都不为过)必须在 Siri 遥控器或 iPhone Siri 遥控器小部件的边缘滑动,

因此请尝试在这些黄色区域上滑动,尽量不要用手指轻敲然后滑动,而是轻轻地向外滑动,让手指完全离开远程边缘

预期结果:

我在成功捕获滑动之前尝试了 100 多次(显然不是用于生产的东西),希望 tvOS 15 及更高版本能够解决这个问题

另一种(更可靠但更老套)的方法是使用 GameController 低级 x/y 值进行方向键控制,

Siri 遥控器也被认为是游戏控制器,它是第一个设置到 apple tv 连接的游戏控制器,

所以在出现 SwiftUI 视图时你可以这样做:

import SwiftUI
import GameController

struct SwipeTestView: View
{
    var body: some View
    {
        Text("This can be some full screen image or what not")
            .onAppear(perform: {
                let gcController = GCController.controllers().first
                let microGamepad = gcController!.microGamepad
                microGamepad!.reportsAbsoluteDpadValues = true
                microGamepad!.dpad.valueChangedHandler = { pad, x, y in
                    let fingerDistanceFromSiriRemoteCenter: Float = 0.7
                    let swipeValues: String = "x: \(x), y: \(y), pad.left: \(pad.left), pad.right: \(pad.right), pad.down: \(pad.down), pad.up: \(pad.up), pad.xAxis: \(pad.xAxis), pad.yAxis: \(pad.yAxis)"
                    
                    if y > fingerDistanceFromSiriRemoteCenter
                    {
                        print(">>> up \(swipeValues)")
                    }
                    else if y < -fingerDistanceFromSiriRemoteCenter
                    {
                        print(">>> down \(swipeValues)")
                    }
                    else if x < -fingerDistanceFromSiriRemoteCenter
                    {
                        print(">>> left \(swipeValues)")
                    }
                    else if x > fingerDistanceFromSiriRemoteCenter
                    {
                        print(">>> right \(swipeValues)")
                    }
                    else
                    {
                        //print(">>> tap \(swipeValues)")
                    }
                }
            })

            .focusable() // <-- this is required only if you want to capture 'press' and 'LongPress'
        
            .onLongPressGesture(minimumDuration: 1, perform: { // on press action
                print(">>> Long press")
            })

            .onLongPressGesture(minimumDuration: 0.01, perform: { // on press action
                print(">>> press")
            })
    }
}

这是一个更可靠的解决方案并且每次都有效,您所要做的就是从 Siri 遥控器的中心向外滑动手指到您想要的滑动方向(上/下/左/右),

你也可以用这种方式实现上+左、上+右、下+左、下+右、顺时针圆周滑动或逆时针圆周滑动以及任何你想要的方式。

您甚至 可能 能够使用 simultaneousGesture()

实现放大手势等
  • 注意:[12.SEP.2021] 如果您打算 运行 模拟器上的此代码,请知道目前模拟器还不支持控制器作为 GameController 并且行:GCController.controllers().first 将 return nil,你需要一个真正的硬件来尝试,

我基于此编写了几个扩展并进行了测试(tvOS 14.7),这是一个可以用作 tvOS 的 SwipeGesture 的扩展:

import SwiftUI
import GameController

// MARK: - View+swipeGestures
struct SwipeGestureActions: ViewModifier
{
    // swipeDistance is how much x/y values needs to be acumelated by a gesture in order to consider a swipe (the distance the finger must travel)
    let swipeDistance: Float = 0.7
    // how much pause in milliseconds should be between gestures in order for a gesture to be considered a new gesture and not a remenat x/y values from the previous gesture
    let secondsBetweenInteractions: Double = 0.2
    
    // the closures to execute when up/down/left/right gesture are detected
    var onUp: () -> Void = {}
    var onDown: () -> Void = {}
    var onRight: () -> Void = {}
    var onLeft: () -> Void = {}

    @State var lastY: Float = 0
    @State var lastX: Float = 0
    @State var totalYSwipeDistance: Float = 0
    @State var totalXSwipeDistance: Float = 0
    @State var lastInteractionTimeInterval: TimeInterval = Date().timeIntervalSince1970
    @State var isNewSwipe: Bool = true
    
    func resetCounters(x: Float, y: Float)
    {
        isNewSwipe = true
        lastY = y // start counting from the y point the finger is touching
        totalYSwipeDistance = 0
        lastX = x // start counting from the x point the finger is touching
        totalXSwipeDistance = 0
    }

    func body(content: Content) -> some View
    {
        content
            .onAppear(perform: {
                let gcController = GCController.controllers().first
                let microGamepad = gcController!.microGamepad
                microGamepad!.reportsAbsoluteDpadValues = false // assumes the location where the user first touches the pad is the origin value (0.0,0.0)
                let currentHandler = microGamepad!.dpad.valueChangedHandler
                microGamepad!.dpad.valueChangedHandler = { pad, x, y in
                    // if there is already a hendler set - execute it as well
                    if currentHandler != nil {
                        currentHandler!(pad, x, y)
                    }
                    
                    /* check how much time passed since the last interaction on the siri remote,
                     * if enough time has passed - reset counters and consider these coming values as a new gesture values
                     */
                    let nowTimestamp = Date().timeIntervalSince1970
                    let elapsedNanoSinceLastInteraction = nowTimestamp - lastInteractionTimeInterval
                    lastInteractionTimeInterval = nowTimestamp // update the last interaction interval
                    if elapsedNanoSinceLastInteraction > secondsBetweenInteractions
                    {
                        resetCounters(x: x, y: y)
                    }
                    
                    /* accumelate the Y axis swipe travel distance */
                    let currentYSwipeDistance = y - lastY
                    lastY = y
                    totalYSwipeDistance = totalYSwipeDistance + currentYSwipeDistance
                    
                    /* accumelate the X axis swipe travel distance */
                    let currentXSwipeDistance = x - lastX
                    lastX = x
                    totalXSwipeDistance = totalXSwipeDistance + currentXSwipeDistance
                    
//                    print("y: \(y), x: \(x), totalY: \(totalYSwipeDistance) totalX: \(totalXSwipeDistance)")
                    
                    /* check if swipe travel goal has been reached in one of the directions (up/down/left/right)
                     * as long as it is consedered a new swipe (and not a swipe that was already detected and executed
                     * and waiting for a few milliseconds stop between interactions)
                     */
                    if (isNewSwipe)
                    {
                        if totalYSwipeDistance > swipeDistance && totalYSwipeDistance > 0 // swipe up detected
                        {
                            isNewSwipe = false // lock so next values will be disregarded until a few milliseconds of 'remote silence' achieved
                            onUp() // execute the appropriate closure for this detected swipe
                        }
                        else if totalYSwipeDistance < -swipeDistance && totalYSwipeDistance < 0 // swipe down detected
                        {
                            isNewSwipe = false
                            onDown()
                        }
                        else if totalXSwipeDistance > swipeDistance && totalXSwipeDistance > 0 // swipe right detected
                        {
                            isNewSwipe = false
                            onRight()
                        }
                        else if totalXSwipeDistance < -swipeDistance && totalXSwipeDistance < 0 // swipe left detected
                        {
                            isNewSwipe = false
                            onLeft()
                        }
                        else
                        {
                            //print(">>> tap")
                        }
                    }
                }
            })
    }
}

extension View
{
    func swipeGestures(onUp: @escaping () -> Void = {},
                       onDown: @escaping () -> Void = {},
                       onRight: @escaping () -> Void = {},
                       onLeft: @escaping () -> Void = {}) -> some View
    {
        self.modifier(SwipeGestureActions(onUp: onUp,
                                          onDown: onDown,
                                          onRight: onRight,
                                          onLeft: onLeft))
    }
}

你可以这样使用它:

struct TVOSSwipeTestView: View
{
    @State var markerX: CGFloat = UIScreen.main.nativeBounds.size.width / 2
    @State var markerY: CGFloat = UIScreen.main.nativeBounds.size.height / 2
    
    var body: some View
    {
        VStack
        {
            Circle()
                .stroke(Color.white, lineWidth: 5)
                .frame(width: 40, height: 40)
                .position(x: markerX, y: markerY)
                .animation(.easeInOut(duration: 0.5), value: markerX)
                .animation(.easeInOut(duration: 0.5), value: markerY)
        }
            .background(Color.blue)
            .ignoresSafeArea(.all)
            .edgesIgnoringSafeArea(.all)
            .swipeGestures(onUp: {
                print("onUp()")
                markerY = markerY - 40
            },
                           onDown: {
                print("onDown()")
                markerY = markerY + 40
            },
                           onRight: {
                print("onRight()")
                markerX = markerX + 40
            },
                           onLeft: {
                print("onLeft()")
                markerX = markerX - 40
            })
        
            .focusable() // <-- this is required only if you want to capture 'press' and 'LongPress'
        
            .onLongPressGesture(minimumDuration: 1, perform: { // on press action
                print(">>> Long press")
            })

            .onLongPressGesture(minimumDuration: 0.01, perform: { // on press action go to middle of the screen
                markerX = UIScreen.main.nativeBounds.size.width / 2
                markerY = UIScreen.main.nativeBounds.size.height / 2
            })
    }
}