fhe
fhe

Reputation: 6187

Undesired interplay between tapable, movable items and scrolling in SwiftUI List

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.

Upvotes: 0

Views: 361

Answers (2)

alinder
alinder

Reputation: 216

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

Upvotes: 4

user1046037
user1046037

Reputation: 17695

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

Upvotes: 0

Related Questions