0

I'm working on a SwiftUI list that shows tapable and long-pressable full-width items, which are movable, and allow for detail navigation.

I've noticed that .onLongPressGesture isn't detected when the list allows for moving of items, because the List switches to drag-moving the long-pressed item instead.

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    let data = Array(0..<20)

    var body: some View {
        NavigationStack {
            List {
                ForEach(data, id:\.self) { item in
                    NavigationLink(destination: EmptyView(), label: {
                        Rectangle().fill(.mint)
                            .onTapGesture { print("tapped", item)  }
                            .onLongPressGesture{ print("longPressed", item)}
                    })
                }.onMove(perform: moveItems)
            }
        }
    }

    func moveItems(from source: IndexSet, to destination: Int) { }
}

PlaygroundPage.current.setLiveView(ContentView())

I've experimented further and found that using simultaneous gesture via simultaneousGesture() fixes the missing notification on long presses, but instead removes scrolling ability from the List.

import SwiftUI
import PlaygroundSupport

struct ContentViewSimultaneous: View {
    let data = Array(0..<20)

    var body: some View {
        NavigationStack {
            List {
                ForEach(data, id:\.self) { item in
                    NavigationLink(destination: EmptyView(), label: {
                        Rectangle().fill(.blue)
                            .simultaneousGesture(TapGesture().onEnded { print("tapped", item) })
                            .simultaneousGesture(LongPressGesture().onEnded { _ in
                                print("longPressed", item) })
                    })
                }.onMove(perform: moveItems)
            }
        }
    }

    func moveItems(from source: IndexSet, to destination: Int) { }
}

PlaygroundPage.current.setLiveView(ContentViewSimultaneous())

I'm now looking for a way to make this work and would appreciate any insights! I'm new to SwiftUI and might miss something important.

fhe
  • 6,099
  • 1
  • 41
  • 44
  • So when you long press what do you intend on doing? Would you like to show some buttons? If so you could use menu button and you also have primary action. Refer - https://developer.apple.com/documentation/swiftui/menu – user1046037 Aug 05 '22 at 08:41
  • @user1046037 I want to change the color of the rectangle to a different color on long press than on tap. It would also be okay to somehow supress the drag-moving in the list and just rely on the moving feature when in edit mode. – fhe Aug 05 '22 at 09:41

2 Answers2

4

I think I was able to get this working as you describe. It works with no issues on iOS 15, but there seems to be an animation bug in iOS 16 that causes the rearrange icon not to animate in for some/all List rows. Once you drag an item in edit mode, the icon will display.

struct ContentView: View {
    
    @State private var editMode: EditMode = .inactive
    @State var disableMove: Bool = true

    var body: some View {
        
        let data = Array(0..<20)
        
        NavigationView {
            List {
                ForEach(data, id:\.self) { item in
                    NavigationLink(destination: EmptyView(), label: {
                        Rectangle().fill(.mint)

                            .onTapGesture { print("tapped", item)  }
                            .onLongPressGesture{ print("longPressed", item)}
                    })
                    
                }
                .onMove(perform: disableMove ? nil : moveItems)
            }
            .toolbar {
                ToolbarItem {
                    
                    Button {
                        withAnimation {
                            self.disableMove.toggle()
                        }
                    } label: {
                        Text(editMode == .active ? "Done" : "Edit")
                    }
                    
                }
            }
            .environment(\.editMode, $editMode)
           
        }
        .onChange(of: disableMove) { disableMove in
            withAnimation {

                self.editMode = disableMove ? .inactive : .active
            
            }
        }
        .navigationViewStyle(.stack)
        
    }
    
    func moveItems(from source: IndexSet, to destination: Int) { }
    
}
alinder
  • 186
  • 7
  • Thanks so much! It's working as expected — except for the move icon not displaying in edit mode on iOS 16 beta 4. I thought I tried all possible permutations, but disabling the move in non-edit mode definitely does the trick. – fhe Aug 05 '22 at 19:26
0

Not sure if this helps

enum Status {
    case notPressed
    case pressed
    case longPressed
}

struct ContentView: View {
    @State private var status = Status.notPressed
    
    var body: some View {
        Rectangle()
            .foregroundColor(color)
            .simultaneousGesture(LongPressGesture().onEnded { _ in
                print("longPressed")
                status = .longPressed
            })
            .simultaneousGesture(TapGesture().onEnded { _ in
                print("pressed")
                status = .pressed
            })
    }
    
    var color: Color {
        switch status {
        case .notPressed:
            return .mint
        case .pressed:
            return .yellow
        case .longPressed:
            return .orange
        }
    }
}
user1046037
  • 16,755
  • 12
  • 92
  • 138
  • Using this View in a List as outlined in my second example shows the same behavior of the List not responding to scroll anymore. The problem is not setting the color, but the different gestures interfering with each other in unintended ways. – fhe Aug 05 '22 at 12:11
  • Ah ok, sorry about that, I guess `simultaneousGesture` is messing it up, Till the time you figure out the long press gesture issue you could build a 3 state button, state 1 when not pressed, press once (state2) press 2nd time (state3), and then states cycles. Not sure if that workaround will help. If you happen to figure out the long press gesture post a solution, would be interested to know – user1046037 Aug 05 '22 at 15:11
  • No problem, appreciate your intention to help (and the clean code example)! Good idea about the 3 state button — in fact, I'm already using it for the single tap to cycle between three states (set, partially set, unset) and wanted to use the long press for a 4th state that is too special to have it in the cycle. – fhe Aug 05 '22 at 15:23
  • Have you had a look at Menu as shown in https://www.hackingwithswift.com/quick-start/swiftui/how-to-show-a-menu-when-a-button-is-pressed. Completely different to what you were asking but long press will come into play and user can choose any button – user1046037 Aug 05 '22 at 15:39
  • Yes, I've also tried the Menu and it also prevents the list from scrolling. It would be a nice alternative solution, though. – fhe Aug 05 '22 at 19:37