如何在SwiftUI中检测视图的向上、向下、向左和向右滑动

61

我开始学习开发苹果手表应用。

我目前正在开发的项目需要检测四个主要方向()的滑动操作。

问题是我不知道如何检测。我已经搜索过了,但却陷入了死胡同。

在下面的视图中,我该怎么做才能在用户向滑动视图时仅打印出swiped up

struct MyView: View {
    var body: some View {
        Text("Hello, World!")
    }
}
感谢。
10个回答

74

您可以使用DragGesture

.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
                    .onEnded({ value in
                        if value.translation.width < 0 {
                            // left
                        }

                        if value.translation.width > 0 {
                            // right
                        }
                        if value.translation.height < 0 {
                            // up
                        }

                        if value.translation.height > 0 {
                            // down
                        }
                    }))

我会试一试!谢谢 :) 看起来正是我所需要的! - Barry Michael Doyle
11
这看起来不错,但是并不能正常工作。除非你是机器人,否则几乎不可能不生成多个响应。 - user3069232
如果您需要滚动视图正常工作,则minimumDistance距离应大于零。 - Muhammad Abbas

56
在其他解决方案在实际设备上有些不一致的情况下,我决定提出另一个解决方案,这个方案似乎在不同的屏幕尺寸上更加一致,因为除了minimumDistance之外没有硬编码的值。
.gesture(DragGesture(minimumDistance: 20, coordinateSpace: .global)
            .onEnded { value in
                let horizontalAmount = value.translation.width
                let verticalAmount = value.translation.height
                
                if abs(horizontalAmount) > abs(verticalAmount) {
                    print(horizontalAmount < 0 ? "left swipe" : "right swipe")
                } else {
                    print(verticalAmount < 0 ? "up swipe" : "down swipe")
                }
            })

38

根据Benjamin的回答,这是一种处理这些情况更为迅速的方法。

.gesture(DragGesture(minimumDistance: 3.0, coordinateSpace: .local)
    .onEnded { value in
        print(value.translation)
        switch(value.translation.width, value.translation.height) {
            case (...0, -30...30):  print("left swipe")
            case (0..., -30...30):  print("right swipe")
            case (-100...100, ...0):  print("up swipe")
            case (-100...100, 0...):  print("down swipe")
            default:  print("no clue")
        }
    }
)

这是“正确”的解决方案,所以非常感谢@vadian提供它 :) - Benjamin B.

32

如果您想得到更“宽容”的滑动方向,可以使用一些条件语句来帮助平衡:

编辑:进行了更多测试,显然第二个条件语句的值会产生一些混淆,因此我调整了它们以消除所述混淆并使手势无懈可击(拖动到角落现在将出现“无线索”而不是其中之一手势)...

let detectDirectionalDrags = DragGesture(minimumDistance: 3.0, coordinateSpace: .local)
.onEnded { value in
    print(value.translation)
    
    if value.translation.width < 0 && value.translation.height > -30 && value.translation.height < 30 {
        print("left swipe")
    }
    else if value.translation.width > 0 && value.translation.height > -30 && value.translation.height < 30 {
        print("right swipe")
    }
    else if value.translation.height < 0 && value.translation.width < 100 && value.translation.width > -100 {
        print("up swipe")
    }
    else if value.translation.height > 0 && value.translation.width < 100 && value.translation.width > -100 {
        print("down swipe")
    }
    else {
        print("no clue")
    }

在视图中应该把这个变量放在哪里? - Soylent Graham
@soylentgraham 将此代码放置在变量体和返回语句之间的结构体中:some View {。默认情况下,如果您在此部分没有返回语句,SwiftUI 将返回第一个 SwiftUI 元素(第一个 ZStack/HStack 等)。但是,如果您在第一个 SwiftUI 元素之前加上 "return",则可以添加通常会在 var body some view 之前添加时出现错误的变量。 - Benjamin B.
7
我很震惊,定义这样的行为需要非常明确和容易出错的构造。在横向和纵向方向上滑动都感觉良好吗?这似乎是API中的一个重大缺陷。 - neuralmer
什么是最适合向左、向右、向上和向下滑动的翻译宽度/高度,以及如何使其“相对于所使用的设备尺寸(紧凑/常规)”?这里固定为30/100,但这是否与苹果用于其应用程序的相同? - JeanNicolas
我只是想回应@JeanNicolas在上面提出的问题。苹果的文档没有提供任何帮助,无法确定所呈现解决方案中数字30或100代表什么,或者这些值应该是什么以实现在所有苹果设备上获得有用功能。 - KeithB
我只是想回应一下@JeanNicolas上面提出的问题。苹果的文档对于确定所提供解决方案中的数字30或100到底意味着什么,或者这些值应该是什么以实现在所有苹果设备上获得有用功能方面没有提供任何帮助。 - undefined

9
创建一个扩展:
extension View {
    func swipe(
        up: @escaping (() -> Void) = {},
        down: @escaping (() -> Void) = {},
        left: @escaping (() -> Void) = {},
        right: @escaping (() -> Void) = {}
    ) -> some View {
        return self.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
            .onEnded({ value in
                if value.translation.width < 0 { left() }
                if value.translation.width > 0 { right() }
                if value.translation.height < 0 { up() }
                if value.translation.height > 0 { down() }
            }))
    }
}

那么:

            Image() // or any View
                .swipe(
                    up: {
                        // action for up
                    },
                    right: {
                        // action for right
                    })

请注意,每个方向都是可选参数。

5
我会为简洁性创建一个修改器。使用方法看起来像这样:
yourView
        .onSwiped(.down) {
            // Action for down swipe
        }

或者

yourView
        .onSwiped { direction in 
            // React to detected swipe direction
        }

您还可以使用trigger参数来配置接收更新的方式:连续接收或仅在手势结束时接收。

完整代码如下:

struct SwipeModifier: ViewModifier {
    enum Directions: Int {
        case up, down, left, right
    }

    enum Trigger {
        case onChanged, onEnded
    }

    var trigger: Trigger
    var handler: ((Directions) -> Void)?

    func body(content: Content) -> some View {
        content.gesture(
            DragGesture(
                minimumDistance: 24,
                coordinateSpace: .local
            )
            .onChanged {
                if trigger == .onChanged {
                    handle($0)
                }
            }.onEnded {
                if trigger == .onEnded {
                    handle($0)
                }
            }
        )
    }

    private func handle(_ value: _ChangedGesture<DragGesture>.Value) {
        let hDelta = value.translation.width
        let vDelta = value.translation.height

        if abs(hDelta) > abs(vDelta) {
            handler?(hDelta < 0 ? .left : .right)
        } else {
            handler?(vDelta < 0 ? .up : .down)
        }
    }
}

extension View {
    func onSwiped(
        trigger: SwipeModifier.Trigger = .onChanged,
        action: @escaping (SwipeModifier.Directions) -> Void
    ) -> some View {
        let swipeModifier = SwipeModifier(trigger: trigger) {
            action($0)
        }
        return self.modifier(swipeModifier)
    }
    func onSwiped(
        _ direction: SwipeModifier.Directions,
        trigger: SwipeModifier.Trigger = .onChanged,
        action: @escaping () -> Void
    ) -> some View {
        let swipeModifier = SwipeModifier(trigger: trigger) {
            if direction == $0 {
                action()
            }
        }
        return self.modifier(swipeModifier)
    }
}

它只在TabView中工作。有没有办法让它在每个视图中都能工作? - Muhammad Farooq

3
这个更加响应:
.gesture(
    DragGesture()
        .onEnded { value in
            
            let pi = Double.pi
            
            let direction = atan2(value.translation.width, value.translation.height)
            
            switch direction {
            case (-pi/4..<pi/4): print("down swipe")
            case (pi/4..<pi*3/4): print("right swipe")
            case (pi*3/4...pi), (-pi..<(-pi*3/4)):
                print("up swipe")
            case (-pi*3/4..<(-pi/4)): print("left swipe")
            default:
                print("no clue")
            }
        }

解释:

  • 拖动手势返回一个变化的向量,即value.translation
  • 然后,该方法使用atan2来确定该向量的方向,如下所示

根据value.translation的返回值,atan2的值将如下:

  • π 对应理想的"上"手势
  • -π/2 对应理想的"左"手势
  • 0.0 对应理想的"下"手势
  • π/2 对应理想的"右"手势

现在,我们想将圆分成4个象限,每个象限中间包含上述的每个值。例如,(-π/4...π/4)将包含任何可以被视为"下"方向的近似值。


你的回答可以通过提供额外的支持信息来改进。请[编辑]添加更多细节,比如引用或文档,以便他人确认你的回答正确。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

2

可能有点晚了,但这里有另一种实现方式,它使用OptionSet使其使用方式更像其他各种SwiftUI组件 -

struct Swipe: OptionSet, Equatable {
    
    init(rawValue: Int) {
        self.rawValue = rawValue
    }
    
    let rawValue: Int
    
    fileprivate var swiped: ((DragGesture.Value, Double) -> Bool) = { _, _ in false } // prevents a crash if someone creates a swipe directly using the init
    
    private static let sensitivityFactor: Double = 400 // a fairly arbitrary figure which gives a reasonable response
    
    static var left: Swipe {
        var swipe = Swipe(rawValue: 1 << 0)
        swipe.swiped = { value, sensitivity in
            value.translation.width < 0 && value.predictedEndTranslation.width < sensitivity * sensitivityFactor
        }
        return swipe
    }

    static var right: Swipe {
        var swipe = Swipe(rawValue: 1 << 1)
        swipe.swiped = { value, sensitivity in
            value.translation.width > 0 && value.predictedEndTranslation.width > sensitivity * sensitivityFactor
        }
        return swipe
    }

    static var up: Swipe {
        var swipe = Swipe(rawValue: 1 << 2)
        swipe.swiped = { value, sensitivity in
            value.translation.height < 0 && value.predictedEndTranslation.height < sensitivity * sensitivityFactor
        }
        return swipe
    }

    static var down: Swipe {
        var swipe = Swipe(rawValue: 1 << 3)
        swipe.swiped = { value, sensitivity in
            value.translation.height > 0 && value.predictedEndTranslation.height > sensitivity * sensitivityFactor
        }
        return swipe
    }
    
    static var all: Swipe {
        [.left, .right, .up, .down]
    }
    
    private static var allCases: [Swipe] = [.left, .right, .up, .down]
    
    fileprivate var array: [Swipe] {
        Swipe.allCases.filter { self.contains($0) }
    }
}

extension View {
    
    func swipe(_ swipe: Swipe, sensitivity: Double = 1, action: @escaping (Swipe) -> ()) -> some View {
        
        return gesture(DragGesture(minimumDistance: 30, coordinateSpace: .local)
            .onEnded { value in
                swipe.array.forEach { swipe in
                    if swipe.swiped(value, sensitivity) {
                        action(swipe)
                    }
                }
            }
        )
    }
}

在SwiftUI视图中 -
HStack {
    // content
}
.swipe([.left, .right]) { swipe in // could also be swipe(.left) or swipe(.all), etc
    doSomething(with: swipe)
}

显然,检测滑动的逻辑有点基础,但很容易根据您的要求进行调整。


1
import SwiftUI

struct SwipeModifier: ViewModifier {

let rightToLeftAction: () -> ()
let leftToRightAction: () -> ()

func body(content: Content) -> some View {
    content
        .gesture(DragGesture(minimumDistance: 20, coordinateSpace: .global)
            .onEnded { value in
                let horizontalAmount = value.translation.width
                let verticalAmount = value.translation.height
                
                guard abs(horizontalAmount) > abs(verticalAmount) else {
                    return
                }
                
                withAnimation {
                    horizontalAmount < 0 ? rightToLeftAction() :  leftToRightAction()
                }
            })
    }
}

extension View {
  func swipe(rightToLeftAction: @escaping () -> (), leftToRightAction: @escaping () -> ()) -> some View {
     modifier(SwipeModifier(rightToLeftAction: rightToLeftAction, leftToRightAction: leftToRightAction))
   }
}

改进版本的Fynn Becker的回答。


0
我根据Fynn Becker的答案创建了一个扩展来支持我的使用情况。
typealias SwipeDirectionAction = (SwipeDirection) -> Void

extension View {
  /// Adds an action to perform when swipe end.
  /// - Parameters:
  ///   - action: The action to perform when this swipe ends.
  ///   - minimumDistance: The minimum dragging distance for the gesture to succeed.
  func onSwipe(action: @escaping SwipeDirectionAction, minimumDistance: Double = 20) -> some View {
    self
      .gesture(DragGesture(minimumDistance: minimumDistance, coordinateSpace: .global)
        .onEnded { value in
          let horizontalAmount = value.translation.width
          let verticalAmount = value.translation.height

          if abs(horizontalAmount) > abs(verticalAmount) {
            horizontalAmount < 0 ? action(.left) : action(.right)
          } else {
            verticalAmount < 0 ? action(.up) : action(.bottom)
          }
        })
  }
}

enum SwipeDirection {
  case up, bottom, left, right
}

使用方法:

  var body: some View {
    SpriteView(scene: scene)
      .ignoresSafeArea()
      .onSwipe { direction in
        print(direction)
      }
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接