Managing simultaneous, in sequence and exclusive gestures

Managing simultaneous, in sequence and exclusive gestures

Understand how to compose multiple gestures together to create complex interactions in a SwiftUI app.

To combine gestures together in SwiftUI, you can use structures like SequenceGesture if you need to sequence two gestures, SimultaneousGesture to trigger two gestures at the same time, or even ExclusiveGesture for two gestures where only one of them can succeed. Here we will see one by one.

Sequence of gestures

To use a sequence of gestures, we will create a view that changes color when pressed and can be dragged after a long press. In this way, we will use chained gestures with LongPressGesture + DragGesture.

 LongPressGesture(minimumDuration: 0.5)
    .sequenced(before: DragGesture())
  • First, the user has access to a long press of 0.5 seconds.
  • Then, if the user continues with the gesture, a drag gesture.
struct ContentView: View {
    
    @GestureState private var isPressed = false
    @State private var offset = CGSize.zero

    var body: some View {
        Circle()
            .fill(isPressed ? .orange : .blue)
            .frame(width: 100, height: 100)
            .shadow(color: .primary, radius: 1, x: 4, y: 4)
            .offset(offset)
            .gesture(
                LongPressGesture(minimumDuration: 0.5)
                    .sequenced(before: DragGesture())
                    .updating($isPressed) { value, state, _ in
                        switch value {
                        case .first(true):
                            state = true
                        case .second(true, _):
                            state = true
                        default:
                            state = false
                        }
                    }
                    .onChanged { value in
                        if case .second(true, let drag?) = value {
                            offset = drag.translation
                        }
                    }
            )
    }
}

The .updating() modifier updates the @GestureState while the gesture is occurring, and isPressed temporarily changes to true when long press or drag is active. The .onChanged() method moves the circle when drag is activated after the long press, and the translation provides the offset of the user's gesture relative to the starting point. The treatment of view color and translation (drag) is determined by the .fill() and .offset() modifiers, respectively.

Gestures at the same time

Now, we will combine other gestures at the same time, dragging a view and rotating it, according to the user's gesture, DragGesture + RotateGesture.

      DragGesture()
        .onChanged { value in offset = value.translation }
        .simultaneously(with:
            RotateGesture()
                .onChanged { value in angle = value.rotation }                         
        )

So, you can display a view that:

  • Dragged by a user gesture.
  • Rotated with a twisting gesture (with two fingers on the trackpad or touchscreen, for example).

The two gestures work simultaneously thanks to the use of simultaneously(with:).

struct ContentView: View {
    
    @State private var offset = CGSize.zero
    @State private var angle = Angle.zero
    
    var body: some View {
        Rectangle()
            .fill(.blue)
            .frame(width: 150, height: 150)
            .shadow(color: .primary, radius: 1, x: 4, y: 4)
            .offset(offset)
            .rotationEffect(angle)
            .gesture(
                DragGesture()
                    .onChanged { value in offset = value.translation }
                    .simultaneously(with:
                        RotateGesture()
                        .onChanged { value in angle = value.rotation }
                                    
                                   )
            )
    }
}

The DragGesture structure with the .onChanged() method causes the movement value to be saved in offset, when the user drags the rectangle (chosen view). Meanwhile, simultaneously(with:) allows the user to rotate while dragging. We use RotateGesture with the .onChanged() method as well, to update the rotation angle in real time. The .offset() modifier applies the drag action of the view by the user's gesture and .rotationEffect() applies the rotation based on the value of the angle variable.

One gesture at a time

If your main idea is that only one of the gestures is recognized at a time, that is, the one that is recognized first "excludes" the other, we can do this with TapGesture + LongPressGesture.

ExclusiveGesture(
    TapGesture()
        .onEnded {
            color = .orange
        },
    LongPressGesture(minimumDuration: 1.0)
        .onEnded { _ in
            scale = 1.5
        }
)

You can interact with a view in two unique ways:

  • Tap: change the color.
  • Long press: increase the scale size.
struct ContentView: View {
    
    @State private var color: Color = .blue
    @State private var scale: CGFloat = 1.0
    
    var body: some View {
        Capsule()
            .fill(color)
            .frame(width: 200, height: 100)
            .shadow(color: .primary, radius: 1, x: 4, y: 4)
            .scaleEffect(scale)
            .gesture(
                ExclusiveGesture(
                    TapGesture()
                        .onEnded {
                            color = .orange
                        },
                    LongPressGesture(minimumDuration: 1.0)
                        .onEnded { _ in
                            scale = 1.5
                        }
                )
            )
            .animation(.easeInOut, value: scale)
    }
}

The ExclusiveGesture() structure defines two gestures, but only triggers the first one that detects interaction. In this example, if the user simply taps quickly, the TapGesture will fire and change color to orange, but if the user holds down for 1 second, the LongPressGesture will be recognized, and the .scaleEffect will increase to 1.5.