skip to Main Content

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.

Thanks in advance!


Update:

Thanks for the solution by @nickreps:

Fixed scrolling and long tap

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) { value, state, transaction in
        state = value
        transaction.animation = .easeOut(duration: 0.2)
      }
    
    Text(text)
      .frame(width: 400, height: 200)
      .background(isLongPressure ? .white : color)
      .delaysTouches(for: 0.01) {
        //some code here, if needed
      }
      .gesture(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)
      }
    }
}

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


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

2

Answers


  1. I’m not sure I understand the exact context, but you could add a condition so your LongPressGesture only triggers an action when gesture is not being used for scrolling.

    let longTap = LongPressGesture(minimumDuration: 0.3)
      .updating($isLongPressure) { value, state, transaction in
        if value {
          state = true
          transaction.animation = .easeOut(duration: 0.2)
        }
      }
    
    Login or Signup to reply.
  2. 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
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search