swiftuiswiftui-swipeactions

SwiftUI swipeActions reload results in leading blank space and broken swipe


I am trying to use the swipeActions SwiftUI modifier setup as displayed in the code below but the swipe action gets disabled as seen on this gif:
enter image description here

struct ContentView: View {
@ObservedObject var viewModel: ViewModel

var body: some View {
    if viewModel.items.count > 0 {
        ZStack {
            List {
                ForEach(viewModel.items, id: \.self) { item in
                    Text(item)
                        .swipeActions {
                            Button {
                                viewModel.removeAction(item: item)
                            } label: {
                                Text("Remove")
                            }
                            .tint(.orange)
                        }
                }
            }
            
        }
    } else {
        ProgressView()
            .foregroundColor(.accentColor)
            .scaleEffect(2)
    }
}

In the view model after the first swipe, I would reload the list from the API (the sample code just mocks a delay):

extension ContentView {
class ViewModel: ObservableObject {
    @Published var items: [String]
    
    init(items: [String]) {
        self.items = items
    }
    
    func removeAction(item: String) {
        if let index = items.firstIndex(where: { $0 == item }) {
            items.remove(at: index)
        }
        
        let itemsSaved = items
        items = []
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.items = itemsSaved
        }
    }
}

Expected behaviour: the reloaded rows do not have a space view at the beginning of each row, and the rows can be swiped as before.

Actual behaviour: each rows has a space view at the beginning of the row, you cannot swipe the rows as before.

enter image description here

I created a sample project also: code and additional video.

Any idea if there is workaround?

Thanks.


Solution

  • I am still not sure why your implementation is causing this behavior, other than that you are completely switching between two separate views(Zstack vs. ProgressView). My suspicion is that the change back and forth is simply putting the List into some weird state. The fix, however, is simple; put the conditional inside of the ZStack:

    struct ContentView: View {
        @ObservedObject var viewModel: ViewModel
    
        var body: some View {
            ZStack {
                 // Move the conitional inside of the ZStack.
                 // Always use .isEmpty for this sort of test. Faster and
                 // less resources than count
                 if !viewModel.items.isEmpty {
                      List {
                          // I made Item an Identifiable struct. Deleting items
                          // identified as .self can lead to issues in a ForEach
                          ForEach(viewModel.items) { item in
                               Text(item.name)
                                   .swipeActions {
                                        Button {
                                            viewModel.removeAction(item: item)
                                   } label: {
                                        Text("Remove")
                                   }
                                        .tint(.orange)
                               }
                          }
                      }
                 } else {
                      Text("Progress View")
                 }
             }
        }
    }
    
    extension ContentView {
        class ViewModel: ObservableObject {
            @Published var items: [Item]
            
            init(items: [Item]) {
                self.items = items
            }
            
            func removeAction(item: Item) {
                if let index = items.firstIndex(where: { $0 == item }) {
                    items.remove(at: index)
                }
                
                let itemsSaved = items
                items = []
                
                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                    self.items = itemsSaved
                }
            }
        }
    }
    
    // I made this as a data model. 
    struct Item: Identifiable, Hashable {
        var id = UUID()
        var name: String
    }