How to implement a left or right DragGesture() that trigger a switch case in SwiftUI?
Asked Answered
S

6

6

I have created a DragGesture in a View that should select a @State(Bool) whether the user swipe left or right.

The thing is that only swiping right is detected.

How to use .gesture() to capture whether a user is swiping left or right on his screen?

import SwiftUI

struct SwiftUIView: View {

  //MARK: Value to change on swipe gesture
  @State var swipeRight: Bool

  var body: some View {
    VStack {
      //MARK: Value displayed after user swiped
      Text($swipeRight ? "Right" : "Left")
    }
    .gesture(
      DragGesture()
        .onChanged { (value) in
          //MARK: What is it missing here?
          switch value.location.x {
          case ...(-0.5):
            self.swipeRight = false
            print("Swipe Left return false")
          case 0.5...:
            self.swipeRight = true
            print("Swipe Right return true")
          default: ()
          }
    })
}
Sundin answered 29/11, 2019 at 17:42 Comment(0)
S
0

I made a small improvement in @emehex code that will allow you to choose direction

extension View {
    public func dismissingGesture(tolerance: Double = 24, direction: DragGesture.Value.Direction, action: @escaping () -> ()) -> some View {
        gesture(DragGesture()
            .onEnded { value in
                let swipeDirection = value.detectDirection(tolerance)
                if swipeDirection == direction {
                    action()
                }
            }
        )
    }
}

extension DragGesture.Value {
    func detectDirection(_ tolerance: Double = 24) -> Direction? {
        if startLocation.x < location.x - tolerance { return .left }
        if startLocation.x > location.x + tolerance { return .right }
        if startLocation.y > location.y + tolerance { return .up }
        if startLocation.y < location.y - tolerance { return .down }
        return nil
    }

    enum Direction {
        case left
        case right
        case up
        case down
    }
}

 .dismissingGesture(direction: .right){
                // perform action
            }
Secondhand answered 6/7, 2023 at 14:53 Comment(1)
I would update gesture to be a simultaneousGesture as they can be stack on each others on a View when using it.Sundin
K
10

Swift 5, iOS 13

An improved version presented by [Mojtaba Hosseini ][1] here.

[1]: https://stackoverflow.com/users/5623035/mojtaba-hosseini. Place the enum and function before the body of ContentView.

enum SwipeHVDirection: String {
    case left, right, up, down, none
}

func detectDirection(value: DragGesture.Value) -> SwipeHVDirection {
if value.startLocation.x < value.location.x - 24 {
            return .left
          }
          if value.startLocation.x > value.location.x + 24 {
            return .right
          }
          if value.startLocation.y < value.location.y - 24 {
            return .down
          }
          if value.startLocation.y > value.location.y + 24 {
            return .up
          }
  return .none
  }

...

With it called inside a DragGesture. Calling it on onEnded to stop it firing multiple times.

.gesture(DragGesture()
        .onEnded { value in
        print("value ",value.translation.width)
          let direction = self.detectDirection(value: value)
          if direction == .left {
            // your code here
          }  
        }
      )

Obviously you need/can add other directions as needed...

Kaduna answered 14/5, 2020 at 19:44 Comment(2)
What's the 24 values on each if statement? I used this solution and it works great for a left nav swipe gesture feature.Thenna
The magic number 24, sorry is the amount of travel that the cursor needs to make to register the swipe.Kaduna
K
7

You should compare old and new locations instead:

if value.startLocation.x > value.location.x {
    print("Swipe Left")
} else {
    print("Swipe Right")
}

So the refactored version of your code would be:

struct ContentView: View {
    enum SwipeHorizontalDirection: String {
        case left, right, none
    }

    @State var swipeHorizontalDirection: SwipeHorizontalDirection = .none { didSet { print(swipeHorizontalDirection) } }

    var body: some View {
        VStack {
            Text(swipeHorizontalDirection.rawValue)
        }
        .gesture(
            DragGesture()
                .onChanged {
                    if $0.startLocation.x > $0.location.x {
                        self.swipeHorizontalDirection = .left
                    } else if $0.startLocation.x == $0.location.x {
                        self.swipeHorizontalDirection = .none
                    } else {
                        self.swipeHorizontalDirection = .right
                    }
        })
    }
}
Killoran answered 29/11, 2019 at 17:57 Comment(0)
A
3

Extend DragGesture.Value:

import SwiftUI

extension DragGesture.Value {
    func detectDirection(_ tolerance: Double = 24) -> Direction? {
        if startLocation.x < location.x - tolerance { return .left }
        if startLocation.x > location.x + tolerance { return .right }
        if startLocation.y > location.y + tolerance { return .up }
        if startLocation.y < location.y - tolerance { return .down }
        return nil
    }
    
    enum Direction {
        case left
        case right
        case up
        case down
    }
}

Add a dismissingGesture method to View:

extension View {
    public func dismissingGesture(tolerance: Double = 24, action: @escaping () -> ()) -> some View {
        gesture(DragGesture()
            .onEnded { value in
                let direction = value.detectDirection(tolerance)
                if direction == .left {
                    action()
                }
            }
        )
    }
}

Example usage:

struct DismissingGesture_Previews: PreviewProvider {
    static var previews: some View {
        Preview()
    }
    
    struct Preview: View {
        var body: some View {
            NavigationView {
                NavigationLink {
                    Destination()
                } label: {
                    Text("Go")
                }
            }
        }
    }
    
    struct Destination: View {
        @Environment(\.dismiss) private var dismiss
        
        var body: some View {
            ZStack {
                Color.yellow.edgesIgnoringSafeArea(.all)
                Text("Nothing")
            }
            .dismissingGesture {
                dismiss()
            }
            .navigationBarHidden(true)
        }
    }
}
Afflict answered 23/4, 2022 at 23:8 Comment(0)
D
2

I think the directions above were inverted? Or I'm misunderstanding swipe left and right. Anyway, here's a further refinement using view modifiers. You can just at .onSwipe { direction in ... } to your views.

struct SwipeModifier: ViewModifier {
    let action: ((UISwipeGestureRecognizer.Direction) -> Void)?

    init(perform action: ((UISwipeGestureRecognizer.Direction) -> Void)? = nil) {
        self.action = action
    }
        
    func body(content: Content) -> some View {
        content
            .gesture(DragGesture(minimumDistance: 24.0, coordinateSpace: .local)
                        .onEnded { value in
                            guard let action = action else {
                                return
                            }
                            if value.startLocation.x > value.location.x {
                                action(.left)
                            } else if value.startLocation.x < value.location.x {
                                action(.right)
                            } else if value.startLocation.y > value.location.y {
                                action(.down)
                            } else if value.startLocation.y < value.location.y {
                                action(.up)
                            }
                        })
    }
}

extension View {
    public func onSwipe(perform action: ((UISwipeGestureRecognizer.Direction) -> Void)? = nil) -> some View {
        return self.modifier(SwipeModifier(perform: action))
    }
}
Destroy answered 8/10, 2020 at 12:14 Comment(1)
This code has an issue and will not produce the desired effect. If a user now tries to do an upward drag and their finger moves even 1 point to the left or right during that drag, the left or right conditions will evaluate as true, as long as the minimum drag distance was 24.0. You need to leave the subtrahend/addend on value.location as per the solutions above to prevent this undesirable behaviour.Indicia
C
1

Here is a very straight forward example that detects a drag over a certain minimum distance and then decides in which direction the drag was most.

  • Decide if drag was more vertical or horizontal

  • Decide if drag was more up/down or left/right.

    .gesture(DragGesture(minimumDistance: 80, coordinateSpace: .local)
    
         .onEnded({ value in
    
             let yChange = abs(value.startLocation.y - value.location.y)
             let xChange = abs(value.startLocation.x - value.location.x)
    
             if (yChange > xChange) {
    
                 if (value.startLocation.y > value.location.y) {
                     print("up")
                 } else {
                     print("down")
                 }
    
             } else {
    
                 if (value.startLocation.x > value.location.x) {
                     print("left")
                 } else {
                     print("right")
                 }
    
             }
         })
     )
    
Caustic answered 14/11, 2023 at 0:35 Comment(0)
S
0

I made a small improvement in @emehex code that will allow you to choose direction

extension View {
    public func dismissingGesture(tolerance: Double = 24, direction: DragGesture.Value.Direction, action: @escaping () -> ()) -> some View {
        gesture(DragGesture()
            .onEnded { value in
                let swipeDirection = value.detectDirection(tolerance)
                if swipeDirection == direction {
                    action()
                }
            }
        )
    }
}

extension DragGesture.Value {
    func detectDirection(_ tolerance: Double = 24) -> Direction? {
        if startLocation.x < location.x - tolerance { return .left }
        if startLocation.x > location.x + tolerance { return .right }
        if startLocation.y > location.y + tolerance { return .up }
        if startLocation.y < location.y - tolerance { return .down }
        return nil
    }

    enum Direction {
        case left
        case right
        case up
        case down
    }
}

 .dismissingGesture(direction: .right){
                // perform action
            }
Secondhand answered 6/7, 2023 at 14:53 Comment(1)
I would update gesture to be a simultaneousGesture as they can be stack on each others on a View when using it.Sundin

© 2022 - 2024 — McMap. All rights reserved.