15

I am trying to implement a swipe(from left to the right) to edit action using SwiftUI. A delete action(swipe from right to left) and a move item action works perfectly.

I want to open the edit screen on the left to the right guesture

This is my code:

struct TableView : View {
@State var dataSource = DataSource()

var body: some View {
        NavigationView {
            List {
                ForEach(dataSource.pokemons.identified(by: \.id)) { pokemon in
                    Text(pokemon.name) 
                }
                .onDelete(perform: deletePokemon)
                .onMove(perform: movePokemon)
            }
            .navigationBarItems(leading: EditButton(), trailing: Button(action: addPokemon, label: { Text("Add") }))
            .navigationBarTitle(Text("Pokemons"))
        }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
czater
  • 377
  • 4
  • 11

4 Answers4

4

I don't think it is possible currently.

The best suggestion I have is to roll your own solution by using UITableView via the UIViewRepresentable protocol. That being said, there might be viable open-source solutions out there.

I think hoping for all the UITableView features you may want is risky because List is supposed to be a "generic" type that is supported across various platforms. Some features of UITableView may never come to a List.

This is quick code I typed up, but it gives a simple example of how to create a custom UITableView solution:

RoutineTableView(routines: routineDataSource.routines)
  .trailingSwipeActionsConfiguration {
    let editAction = UIContextualAction(
      style: .normal,
      title: "EDIT"
    ) { (action, sourceView, completionHandler) in

      completionHandler(true)
    }
    editAction.backgroundColor = UIColor.darkGray
    let deleteAction = UIContextualAction(
      style: .destructive,
      title: "DELETE"
    ) { (action, sourceView, completionHandler) in

      completionHandler(true)
    }
    let actions = [deleteAction, editAction]
    let configuration = UISwipeActionsConfiguration(actions: actions)
    return configuration
  }
  .onCellPress {
    print("hi there")
  }
  .navigationBarTitle("Routines")
private class CustomDataSource<SectionType: Hashable, ItemType: Hashable>: UITableViewDiffableDataSource<SectionType, ItemType> {

  override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
  }
}

struct RoutineTableView: UIViewRepresentable {

  let routines: [Routine]
  private var onCellPress: (() -> Void)? = nil
  private var trailingSwipeActionsConfiguration: (() -> UISwipeActionsConfiguration)? = nil

  init(routines: [Routine]) {
    self.routines = routines
  }

  func makeUIView(
    context: UIViewRepresentableContext<RoutineTableView>
  ) -> UITableView {
    let tableView = UITableView()
    context.coordinator.update(withTableView: tableView)
    return tableView
  }

  func updateUIView(_ uiView: UITableView, context: UIViewRepresentableContext<RoutineTableView>) {
    context.coordinator.update(routines: routines)
  }

  // MARK: - Coordinator

  func makeCoordinator() -> RoutineTableView.Coordinator {
    return Coordinator(self)
  }

  class Coordinator: NSObject, UITableViewDelegate {

    private enum Section {
      case first
    }

    private let view: RoutineTableView
    private var dataSource: UITableViewDiffableDataSource<Section, Routine>?

    init(_ view: RoutineTableView) {
      self.view = view
      super.init()
    }

    func update(withTableView tableView: UITableView) {
      tableView.register(RoutineTableViewCell.self)
      tableView.delegate = self

      let dataSource = CustomDataSource<Section, Routine>(tableView: tableView) { (tableView, indexPath, routine) -> UITableViewCell? in
        let cell: RoutineTableViewCell = tableView.dequeueReusableCell(for: indexPath)
        cell.configure(withRoutine: routine)
        return cell
      }
      self.dataSource = dataSource
    }

    func update(routines: [Routine]) {
      var snapshot = NSDiffableDataSourceSnapshot<Section, Routine>()
      snapshot.appendSections([.first])
      snapshot.appendItems(routines)
      dataSource?.apply(snapshot, animatingDifferences: true)
    }

    // MARK: - <UITableViewDelegate>

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
      view.onCellPress?()
    }

    func tableView(
      _ tableView: UITableView,
      trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
    ) -> UISwipeActionsConfiguration? {
      return view.trailingSwipeActionsConfiguration?()
    }

  }
}

extension RoutineTableView {

  func onCellPress(
    _ onCellPress: @escaping () -> Void
  ) -> RoutineTableView {
    var view = self
    view.onCellPress = onCellPress
    return view
  }

  func trailingSwipeActionsConfiguration(
    _ trailingSwipeActionsConfiguration: @escaping () -> UISwipeActionsConfiguration
  ) -> RoutineTableView {
    var view = self
    view.trailingSwipeActionsConfiguration = trailingSwipeActionsConfiguration
    return view
  }
}
kgaidis
  • 14,259
  • 4
  • 79
  • 93
  • Yes, I think that there is no SwiftUI solution for this right now, we are forced to use the UIViewRepresentable. Thank you! – czater Dec 03 '19 at 23:17
  • private var trailingSwipeActionsConfiguration: ((IndexPath) -> UISwipeActionsConfiguration)? = nil ... Pass the indexPath along as well. – cvb Oct 19 '20 at 20:27
4

iOS 15+

Starting from iOS 15 you can use swipeActions:

ForEach(dataSource.pokemons.identified(by: \.id)) { pokemon in
    Text(pokemon.name)
}
.swipeActions(edge: .leading) {
    Button("Edit") {
        print("Edit")
    }
    .tint(.blue)
}
.swipeActions(edge: .trailing) {
    Button("Delete", role: .destructive) {
        print("Delete")
    }
    Button("Flag") {
        print("Flag")
    }
    .tint(.orange)
}
pableiros
  • 14,932
  • 12
  • 99
  • 105
pawello2222
  • 46,897
  • 22
  • 145
  • 209
1

Wow! Hmmmm, I'm not sure about using an EditButton() !

I assume you have a list and you want to swipe the row and see a choice to delete right?

All you have to do is implement .onDelete(perform: delete) after the closure for the List. Then, add a function to the structure that defines the delete function in which you handle the closure. Remember that the function will be defined as: func delete (at offsets: IndexSet) {}

Add what I've suggested and compile even without the function body completed (i.e. add a print() placeholder) and you can see the swipe behaviour for delete.

Justin Ngan
  • 1,050
  • 12
  • 20
  • 3
    Correct! As I write, `delete` works perfectly fine for me. But I wonder how can I add more actions like `Edit` or `More`? This is the thing that I am looking for. – czater Sep 04 '19 at 00:28
  • 1
    Sorry, it looks like none of us read your question properly. If you want to have custom response to gestures on a list item see: https://developer.apple.com/documentation/swiftui/list/3270232-itemprovider – Justin Ngan Sep 05 '19 at 01:09
0

You have to use EditButton() instead. It enables edit mode for a List component.

Mecid
  • 4,491
  • 6
  • 30
  • 30
  • I have to use EditButton() instead of what? – czater Sep 02 '19 at 23:25
  • 1
    To enable edit mode, you have to use EditButton or Environment value for editMode. – Mecid Sep 03 '19 at 11:57
  • 3
    I know how to enable edit mode and it works perfectly fine but again I want to enable the swipe to delete/edit gesture that is not working for me. Something like what you have in the default mail app when you can simply swipe an email and mark it as read or delete it. – czater Sep 03 '19 at 22:46