Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Custom Swipe Actions In List

How can I use custom Swipe Actions in SwiftUI?

I tried to use the UIKit Framework to get these working in SwiftUI. But that doesn't work for me.

import SwiftUI
import UIKit



    init() {
        override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
            let important = importantAction(at: indexPath)
            return UISwipeActionsConfiguration(actions: [important])
        }
        func importantAction(at indexPath: IndexPath) -> UIContextualAction {
            let action = UIContextualAction(style: .normal, title: "Important") { (action, view, completion) in
                print("HI")
            }
            action.backgroundColor = UIColor(hue: 0.0861, saturation: 0.76, brightness: 0.94, alpha: 1.0) /* #f19938 */
            action.image = UIImage(named: "pencil")
            return action
        }
    }






struct TestView: View {

      NavigationView {
               List {
                    ForEach(appointmentsViewModel.appointments.identified(by: \.id)) { appointment in Row_Appointments(appointment: appointment)
                }.onDelete(perform: delete)
            }
        }
    }
}

like image 606
Max Avatar asked Jul 19 '19 12:07

Max


5 Answers

If your deployment target is iOS 15 (or newer), then you can use the swipeActions modifier to customize the swipe actions of a list item.

This also applies to watchOS 8 and macOS 12.

These operating systems will be released in late 2021.

Prior to the late 2021 version of SwiftUI, there is no support for custom swipe actions for List items.

If you need to target an older version, you would probably be better off implementing a different user interface, like adding a toggle button as a subview of your list item, or adding a context menu to your list item.

like image 103
rob mayoff Avatar answered Oct 23 '22 16:10

rob mayoff


iOS 15+

In iOS 15 we can finally use native Swipe Actions:

func swipeActions<T>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true, content: () -> T) -> some View where T : View

They can be attached to the ForEach container just like onMove or onDelete:

List {
    ForEach(appointmentsViewModel.appointments.identified(by: \.id)) { appointment in
        Row_Appointments(appointment: appointment)
    }
    .swipeActions(edge: .trailing) {
        Button {
            print("Hi")
        } label: {
            Label("Important", systemImage: "pencil")
        }
    }
}
like image 42
pawello2222 Avatar answered Oct 23 '22 15:10

pawello2222


Based on Michał Ziobro answer using Introspect to simplify table view delegate setup.

Note that this will override the table view delegate and might BREAK some of the existing table view behaviours. While things such as header hight can be fixed by adding the method to custom delegate yourself, other might not be fixable.

struct ListSwipeActions: ViewModifier {

    @ObservedObject var coordinator = Coordinator()

    func body(content: Content) -> some View {

        return content
            .introspectTableView { tableView in
                tableView.delegate = self.coordinator
            }
    }

    class Coordinator: NSObject, ObservableObject, UITableViewDelegate {

        func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
            return .delete
        }

        func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

            let archiveAction = UIContextualAction(style: .normal, title: "Title") { action, view, completionHandler in
                // update data source
                completionHandler(true)
            }
            archiveAction.image = UIImage(systemName: "archivebox")!
            archiveAction.backgroundColor = .systemYellow

            let configuration = UISwipeActionsConfiguration(actions: [archiveAction])

            return configuration
        }
    }
}

extension List {
    func swipeActions() -> some View {
        return self.modifier(ListSwipeActions())
    }
}
like image 5
Ivan Rep Avatar answered Oct 23 '22 15:10

Ivan Rep


It is able to be done in the way something like this:

           List {
                ForEach(items) { (item) in

                    Text("\(item.title)")
                }
                .onDelete(perform: self.delete)
            }.swipeActions()

Then you need to add this swipeActions() modifier

struct ListSwipeActions: ViewModifier {

    @ObservedObject var coordinator = Coordinator()

    func body(content: Content) -> some View {

        return content
            .background(TableViewConfigurator(configure: { tableView in
                delay {
                    tableView.delegate = self.coordinator
                }
            }))
    }

    class Coordinator: NSObject, ObservableObject, UITableViewDelegate {

        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            print("Scrolling ....!!!")
        }

        func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
            return .delete
        }

        func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

            let isArchived = false
            let title = isArchived ? NSLocalizedString("Unarchive", comment: "Unarchive") : NSLocalizedString("Archive", comment: "Archive")

            let archiveAction = UIContextualAction(style: .normal, title: title, handler: {
                (action, view, completionHandler) in

                // update data source
                completionHandler(true)
            })
            archiveAction.title = title
            archiveAction.image = UIImage(systemName: "archivebox")!
            archiveAction.backgroundColor = .systemYellow

            let configuration = UISwipeActionsConfiguration(actions: [archiveAction])

            return configuration
        }
    }
}

extension List {

    func swipeActions() -> some View {
        return self.modifier(ListSwipeActions())
    }
}

And have TableViewConfigurator that searches for table view behind the List

struct TableViewConfigurator: UIViewControllerRepresentable {

    var configure: (UITableView) -> Void = { _ in }

    func makeUIViewController(context: Context) -> UIViewController {

        UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {


        let tableViews = UIApplication.nonModalTopViewController()?.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()

        for tableView in tableViews {
            self.configure(tableView)
        }
    }
}
like image 3
Michał Ziobro Avatar answered Oct 23 '22 15:10

Michał Ziobro


Delighted to see that iOS 15 brings the long awaited .swipeActions view modifier to List in SwiftUI with an easy to use API.

List {
    ForEach(store.messages) { message in
        MessageCell(message: message)
            .swipeActions(edge: .leading) {
                Button { store.toggleUnread(message) } label: {
                    if message.isUnread {
                        Label("Read", systemImage: "envelope.open")
                    } else {
                        Label("Unread", systemImage: "envelope.badge")
                    }
                }
            }
            .swipeActions(edge: .trailing) {
                Button(role: .destructive) {
                    store.delete(message)
                } label: {
                    Label("Delete", systemImage: "trash")
                }
                Button { store.flag(message) } label: {
                    Label("Flag", systemImage: "flag")
                }
            }
        }
    }
}

Actions appear in the order listed, starting from the originating edge working inwards.

The example above produces:

swipe actions

Note that swipeActions override the onDelete handler if provided that is available on ForEach

Read more in Apple's developer docs

like image 2
Dan Barclay Avatar answered Oct 23 '22 16:10

Dan Barclay