Longpress and list scrolling
Asked Answered
Z

4

17

Using SwiftUI (latest XCode and testing on IOS 13.3) I'm trying to implement a long press gesture on items in a list, to allow user interaction with the individual items. The problem is that when I set "onLongPressGesture" anywhere in the list (on items, on the list itself), the list cannot be scrolled anymore. I can easily get a simple tap to work but a long press blocks scrolling.

I've put together a small example that show this issue:

struct ContentView: View
{
  let data = [
    "Test 1","Test 2","Test 3","Test 4","Test 5",
    "Test 6","Test 7","Test 8","Test 9","Test 10",
    "Test 11","Test 12","Test 13","Test 14","Test 15",
    "Test 16","Test 17","Test 18","Test 19","Test 20"
  ]

  var body: some View
  {
    List
    {
      ForEach(data,id:\.self)
      {
        item in
        Text(item).onLongPressGesture{}
      }
    }
  }
}

If I try to drag the list pressing on any text, the list wont move. If I remove the longpress handler, it moves no matter where I press down.

Zinn answered 21/12, 2019 at 22:27 Comment(0)
Z
37

I asked this on the Apple Developer Forum as well and got a solution for the problem. If the view defines an onTapGesture handler before onLongPressGesture, the list will be scrollable while supporting long press on the individual items.

The onTapGesture handler can be empty as long as it is declared first.

struct ContentView: View
{
  let data = [
    "Test 1","Test 2","Test 3","Test 4","Test 5",
    "Test 6","Test 7","Test 8","Test 9","Test 10",
    "Test 11","Test 12","Test 13","Test 14","Test 15",
    "Test 16","Test 17","Test 18","Test 19","Test 20"
  ]

  var body: some View
  {
    List
    {
      ForEach(data,id:\.self)
      {
        item in
        Text(item).onTapGesture{}.onLongPressGesture{}
      }
    }
  }
}
Zinn answered 27/12, 2019 at 11:10 Comment(1)
Unfortunately this seems to add quite a bit of delay before the long press gesture starts. Otherwise, it works.Frazer
K
7

Referring @Jensrodi's solution as it works perfect, although you can experience a higher delay than what you would expect for a Long Press Gesture by adding a .onTapGesture before the .onLongPressGesture.

To mitigate this, you can use onLongPressGesture(minimumDuration:) to reduce/increase to a duration you are comfortable with.

See the example below

List {
    ForEach(0..<100) { x in
        Text("List number -\(x)")
            .onTapGesture{}.onLongPressGesture(minimumDuration: 0.2) { // Setting the minimumDuration to ~0.2 reduces the delay
                print("long press \(x)")
            }
    }
}
Kampmann answered 15/9, 2021 at 4:44 Comment(0)
C
2

I was annoyed by the delay so I created a custom modifier and leveraged the onPressingChanged closure with the custom delay. This way I can trigger the long press action sooner than the native perform: closure is called. Also with this approach, you can add visual feedback (in my case – scale the view on long press).

The usage is simple, just replace onLongPress with the custom modifier:

 Text(item).onTapGesture{}.onScalingLongPressGesture{}

The implementation:

import SwiftUI

extension View {
    func onScalingLongPress(perform action: @escaping () -> Void) -> some View {
        modifier(ScalingLongPressModifier(action: action))
    }
}

struct ScalingLongPressModifier: ViewModifier {
    @State private var longPressTask: Task<Void, Never>?
    @State private var shouldScale: Bool = false
    var scaleWhenPressed: Double = 0.975
    var action: () -> Void
    
    func body(content: Content) -> some View {
        content
            .scaleEffect(shouldScale ? scaleWhenPressed : 1.0)
            .onLongPressGesture(
                minimumDuration: 0.2,
                maximumDistance: 10,
                perform: {
                    // do nothing
                },
                onPressingChanged: { isPressing in
                    handlePressingChange(isPressing: isPressing)
                })
    }
    
    @MainActor
    private func handlePressingChange(isPressing: Bool) {
        if isPressing {
            longPressTask = Task {
                // Wait and scale the view
                try? await Task.sleep(nanoseconds: 200_000_000)
                
                guard !Task.isCancelled else {
                    return
                }
                
                withAnimation(.spring()) {
                    shouldScale = true
                }
                
                // Wait and trigger the action
                try? await Task.sleep(nanoseconds: 200_000_000)
                
                guard !Task.isCancelled else {
                    return
                }
                
                action()
            }
        } else {
            longPressTask?.cancel()
            longPressTask = nil
            
            withAnimation(.spring()) {
                shouldScale = false
            }
        }
    }
}
Christine answered 13/9, 2023 at 10:42 Comment(1)
func onScalingLongPressGesture(_ action : @escaping () -> Void) -> some View { modifier(ScalingLongPressModifier(action: action)) }. to be used as described .Thank you very much for solving this issue . Once more Apple rely on users to provide solutions for the problems they don ' t solve .Backblocks
F
0

I think you should dig around composing with combining gestures. Here you can see, how to compose two and more gestures, but in your case I think you need exclusive behavior (which is described in this article). So you can combine DragGesture and LongPressGesture but for ScrollView (I didn't found any solution for scrolling List). Here are example 1 and example 2 of how to control ScrollView.content.offset (for scrolling on DragGesture).

Fourgon answered 22/12, 2019 at 10:46 Comment(1)
Thanks for the suggestions, it makes for interesting reading and I will definitely look more as I may need this functionality in other areas later. However, I'm not really looking to implement multiple gestures for this. I would like the list view to handle the scrolling it self, giving the correct native feel. I can add an onTapGesture without that blocks the scrolling. But not if I try the long press gesture, which to me doesn't make sense. The basic difference between these two gestures is the time that the finger is pressed without moving.Zinn

© 2022 - 2025 — McMap. All rights reserved.