iosswiftswiftuigesturetap

How to make LongPressGesture and scrolling in ScrollView work together at the same time?


Let's imagine, here is a ScrollView with some elements and I want to make some actions (e.g. changing of color) on long tap on these elements. But also I want to make possible to scroll this view.

Here is an example:

Example of scrolling and long taps together

import SwiftUI

struct TextBox: View {
  var text: String
  var color: Color
  @GestureState private var isLongPressure: Bool = false
  
  var body: some View {
    let longTap = LongPressGesture(minimumDuration: 0.3)
      .updating($isLongPressure) { state, newState, transaction in
        newState = state
        transaction.animation = .easeOut(duration: 0.2)
      }
    
    Text(text)
      .frame(width: 400, height: 200)
      .background(isLongPressure ? .white : color)
      .simultaneousGesture(longTap)
  }
}

struct TestGestures: View {
    var body: some View {
      ScrollView {
        TextBox(text: "Test 1", color: .red)
        TextBox(text: "Test 2", color: .green)
        TextBox(text: "Test 3", color: .blue)
        TextBox(text: "Test 4", color: .red)
        TextBox(text: "Test 5", color: .green)
        TextBox(text: "Test 6", color: .blue)
      }
    }
}

struct TestGestures_Previews: PreviewProvider {
    static var previews: some View {
        TestGestures()
    }
}

So, if I comment .simultaneousGesture(longTap) – scrolling works, but if I uncomment it – scrolling stopped work.

P.S.: I've tried to add onTapGesture before adding longTap and it doesn't help.


Solution

  • I was able to get it working by utilizing a button rather than a TextView. Although this does directly utilize the code you provided, you should be able to modify some pieces to have it meet your needs (I can help with this, if needed!)

    import SwiftUI
    
    struct ScrollTest: View {
        let testData = [1]
        
        var body: some View {
            ScrollView(.vertical, showsIndicators: false) {
                AnimatedButtonView(color: .red, text: "Test 1")
                AnimatedButtonView(color: .green, text: "Test 2")
                AnimatedButtonView(color: .blue, text: "Test 3")
            }
        }
    }
    
    
    
    struct AnimatedButtonView: View {
        @GestureState var isDetectingLongPress = false
        let color: Color
        let text: String
        var body: some View {
            ZStack {
                RoundedRectangle(cornerRadius: 12.5, style: .continuous)
                    .fill(color)
                    .frame(width: UIScreen.main.bounds.width, height: 200)
                    .padding(25)
                    .scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
                    .brightness(!isDetectingLongPress ? 0.0 : -0.125)
                    .animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
                Text(text)
            }
            
            .delaysTouches(for: 0.01) {
                //some code here, if needed
            }
            .gesture(
                LongPressGesture(minimumDuration: 3)
                    .updating($isDetectingLongPress) { currentState, gestureState,
                        transaction in
                        gestureState = currentState
                        transaction.animation = Animation.easeIn(duration: 2.0)
                    }
                    .onEnded { finished in
                        print("gesture ended")
                    })
            
        }
    }
    
    extension View {
        func delaysTouches(for duration: TimeInterval = 0.25, onTap action: @escaping () -> Void = {}) -> some View {
            modifier(DelaysTouches(duration: duration, action: action))
        }
    }
    
    fileprivate struct DelaysTouches: ViewModifier {
        @State private var disabled = false
        @State private var touchDownDate: Date? = nil
        
        var duration: TimeInterval
        var action: () -> Void
        
        func body(content: Content) -> some View {
            Button(action: action) {
                content
            }
            .buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
            .disabled(disabled)
        }
    }
    
    fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
        @Binding var disabled: Bool
        var duration: TimeInterval
        @Binding var touchDownDate: Date?
        
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
                .onChange(of: configuration.isPressed, perform: handleIsPressed)
        }
        
        private func handleIsPressed(isPressed: Bool) {
            if isPressed {
                let date = Date()
                touchDownDate = date
                
                DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
                    if date == touchDownDate {
                        disabled = true
                        
                        DispatchQueue.main.async {
                            disabled = false
                        }
                    }
                }
            } else {
                touchDownDate = nil
                disabled = false
            }
        }
    }