SwiftUI - Custom Swipe Actions In List
Solution 1
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.
Solution 2
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")
}
}
}
Solution 3
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())
}
}
Solution 4
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)
}
}
}
Solution 5
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:
Note that swipeActions
override the onDelete
handler if provided that is available on ForEach
Related videos on Youtube
Max
Updated on September 11, 2021Comments
-
Max almost 3 years
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) } } } }
-
Admin almost 5 yearsYour code - as is - won't build. The
init
is part of something, but what? More, what exactly are you trying to do, and what do you mean by "that doesn't work for me"? Are you saying that theUIViewControllerRepresentable
doesn't work? That things inUIKit
won't work? Maybe that a swipe action is being intercepted by youronDelete
? Please, maybe with more details we can help you. -
rob mayoff almost 5 yearsIt looks like Max wants to add a trailing swipe action to his list item to mark the item “Important”. Since
tableView(_:trailingSwipeActionsConfigurationForRow:)
is aUITableViewDelegate
method, and SwiftUI doesn't let you set the delegate for theUITableView
it creates as an implementation detail, Max's attempt is unlikely to succeed. -
pawello2222 about 3 yearsIn iOS 15 we can finally use native Swipe Actions - see this answer.
-
-
tbergmen about 4 yearsWhat is
delay
used for instruct ListSwipeActions: ViewModifier { func body(content: Content) -> some View {
? -
Michał Ziobro about 4 yearsit is just DispatchQueue.main.asyncAfter(deadline: .now() + interval) { }
-
PRSHL almost 4 yearsWhat is
nonModalTopViewController
? XCode cannot find this -
LetsGoBrandon almost 4 yearsUse of unresolved identifier 'delay' and Type 'UIApplication' has no member 'nonModalTopViewController'
-
LetsGoBrandon almost 4 years@AntonShevtsov really? you tested it?
-
AverageHelper over 3 yearsThis seems to work for regular
willDisplay
callbacks, but nerfs any method that returns a value. Things like section headers will disappear (since UIKit relies on the delegate to host these). -
Koraktor over 3 yearsWhile this works for the swipe actions it essentially breaks any interaction with the rest of the SwiftUI table. Apparently the overridden delegate handles all of this, but re-delegating the methods to the original won‘t help.
-
Ivan Rep over 3 years@Koraktor what specific feature isn't fixable?
-
Koraktor over 3 yearsI have NavigationLinks inside a List. As soon as I use your modifier they stop working. Probably because the new delegate does not handle the taps on the cells.
-
Arvind Patel over 3 yearsprovide a full answer,
-
Marc T. about 3 yearsI tried this and maybe we can get something good out but to be honest to use the scroll view is difficult and it don't come near to what I get if I have a native implementation with UITableView. Did you improve your code In the meanwhile ?