76

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.

11 Answers 11

98

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
                        }
                    }))
Sign up to request clarification or add additional context in comments.

4 Comments

I'll give that a shot! Thank you :) It looks like it's just what I need!
This looks good, but doesn't work correctly. Unless you are a robot is is almost impossible not to generate multiple responses.
if you need that your scroll view should work then minimumDistance distance should be grater than zero.
Receiving an inaccurate result if the inspected value is not tested by 'abs'. Similarly, for (w:1, h: -100), which should indicate "up," your code returns "right."
69

With the other solutions being inconsistent on a physical device,
I decided to create another one that appears to be much more consistent across different screen sizes because there are no hard-coded variables other than 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")
    }
})

Comments

53

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

Comments

33

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

5 Comments

Where should one put this variable in their view?
@soylentgraham put this in the struct between the var body: some View { and the return...by default, swiftUI (if you don't have a return statement in this section) will return the first swiftui element (first ZStack/HStack/etc), but if you put "return" before your first swiftui element, you can add variables that would normally give you an error if added before the var body some view...
I'm shocked that defining behavior like this requires what seems to be very explicit construction, and error-prone construction. Does the swiping "feel nice" on both landscape and portrait orientations? This seems like a big hole in the API.
What are the best translation widths/heights for swiping left, right, up and down and how to make that "relative" to the device size (compact/regular) used? Here fixed with 30/100 but is this the same Apple uses for their apps?
I just want to echo the question that @JeanNicolas asked above. Apple's documentation offers no help in determining what the number 30 or 100 in the presented solutions really means - or what these values should be to gain useful functionality across all Apple devices.
16

Create an extension:

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

Then:

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

Notice that each direction is an optional parameter

Comments

8

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

1 Comment

It is working in the TabView only. Is any way so it can work in every view?
8

This is much more responsive:

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

Explanation:

  • Drag gesture returns a vector of change as value.translation
  • This method then uses atan2 to find the direction of that vector as follows

Based on how value.translation is returned, the values of atan2 will be as follows:

  • π would match an ideal "up" gesture
  • -π/2 is an ideal "left" gesture
  • 0.0 would match an ideal "down" gesture
  • π/2 is an ideal "right" gesture

So now we want to split the circle into 4 quarters, with each quarter including each value above in the middle. For instance (-π/4...π/4) would include any value that could be considered an approximation of a "down" direction.

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
3

Little bit late to this, but here's another implementation which uses OptionSet to make its use a bit more like various other SwiftUI components -

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

In a SwiftUI view -

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

Obviously the logic for detecting swipes is a bit basic, but that's easy enough to tailor to your requirements.

Comments

3

Swipe gestures are not like tap gestures, because it should take velocity into account.

As of iOS 18, Apple has introduced UIGestureRecognizerRepresentable so you can use it to consume UIKit gestures inside SwiftUI. This way you will be able to achieve what you need.

You can use UIGestureRecognizerRepresentable same way as UIViewRepresentable.

Please review: WWDC 2024's "What's new in SwiftUI".

Here is a simple example:

/// The representable struct
struct SwipeGestureRecognizer: UIGestureRecognizerRepresentable {
    let action: (_ direction: UISwipeGestureRecognizer.Direction) -> Void
    
    func makeUIGestureRecognizer(context: Context) -> some UIGestureRecognizer {
        let recognizer = UISwipeGestureRecognizer()
        //TODO: Configure as you would do in UIKit
        return recognizer
    }
    
    func handleUIGestureRecognizerAction(_ recognizer: UISwipeGestureRecognizer, context: Context) {
        if gestureRecognizer.state == .ended {
            action(recognizer.direction)
        }
    }
}

/// Sample view that consumes the SwipeGestureRecognizer
struct ContentView: View {
    var body: some View {
        Rectangle()
            .gesture(SwipeGestureRecognizer { direction in
                // Here is the direction of the gesture
                print(direction)
            })
    }
}

3 Comments

Please add a concise example implementation. Your answer will be much more useful and may well gain a lot of traction.
Answer updated with an example
Thank you for this solution. It appears that the Apple SwiftUI Team decided to not create a native simple swipe recognizer and to instead just provide a link to UIKit. This decision requires developers to learn BOTH SwiftUI AND UIKit to simply recognize a swipe event. For developers (like myself) who have zero familiarity with UIKit, could I ask you to replace the "TODO: Configure as you would do in UIKit" with an actual implementation of a good reliable swipe recognizer using UIKit code. Then your answer would provide a complete solution for developers to re-use.
2

I created an extension to support my use case based on Fynn Becker answer.

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
}

Usage:

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

Comments

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

Improved version of Fynn Becker's answer.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.