How to detect Swiping UP, DOWN, LEFT and RIGHT with SwiftUI on a View

Viewed 20197

I'm getting into building Apple Watch apps.

What I'm currently working on will require me to make use of detecting swipes in the four main directions (UP, DOWN, LEFT and RIGHT)

The problem is I have no idea how to detect this. I've been looking around and I'm reaching dead ends.

What can I do to my view below to just print swiped up when the user swipes UP on the view?

struct MyView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

Thanks.

6 Answers

You could use 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
                        }
                    }))

With the other solutions being a bit inconsistent on a physical device, I decided to come up with another one that seems to be much more consistent across different screen sizes as there are no hardcoded values except for the 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")
                }
            })

If you want one that is more "forgiving" to the directionality of the swipe, you can use a few more conditionals to help even it out:

EDIT: did some more testing, apparently the values for the second conditional add some confusion, so I adjusted them to remove said confusion and make the gesture bulletproof (drags to the corners will now come up with "no clue" instead of one of the gestures)...

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")
    }

Based on Benjamin's answer this is a swiftier way to handle the cases

.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")
        }
    }
)

I would create a modifier for simplicity. Usage will look like that:

yourView
        .onSwiped(.down) {
            // Action for down swipe
        }

OR

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

You can also use trigger parameter in order to configure receiving updates: continuously or only when the gesture ends.

Here's the full code:

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)
    }
}

This is much more responsive:

.gesture(DragGesture(minimumDistance: 3.0, coordinateSpace: .local)
            .onEnded { value in
                let direction = atan2(value.translation.width, value.translation.height)
                switch direction {
                case (-Double.pi/4..<Double.pi/4): self.playMove(.down)
                case (Double.pi/4..<Double.pi*3/4): self.playMove(.right)
                case (Double.pi*3/4...Double.pi), (-Double.pi..<(-Double.pi*3/4)):
                    self.playMove(.up)
                case (-Double.pi*3/4..<(-Double.pi/4)): self.playMove(.left)
                default: 
                    print("unknown)")
                }
            }
Related