In my app, I have many lists where I want the user to be able to reorder the rows via swipeActions
. I also want them to be able to disable editingMode
by tapping anywhere on a row in a List
(it rarely ever makes sense for me to have an EditingButton()
on a Toolbar
).
I've created a class called TitleList
with the described functionality. It takes in an array of strings, and through a ViewBuilder
closure, I can format the array data however I choose. I've mostly accomplished what I'm trying to in the below code:
import SwiftUI
struct HomeView: View
{
@Environment(\.editMode) private var editMode
@State private var titles: [String] = ["Title 1", "Title 2", "Title 3"]
var body: some View
{
NavigationStack
{
VStack
{
TitleList(titles: self.$titles)
{
title in
NavigationLink(destination: Text("Test"))
{
Text(title)
}
/*title in
(Button(action:
{
print("Button Pushed")
})
{
Text(title)
}
.buttonStyle(PlainButtonStyle())
.disabled(self.editMode?.wrappedValue == .active)*/
}
}
}
}
}
struct TitleList<RowContent: View>: View
{
@Environment(\.editMode) private var editMode
@Binding var titles: [String]
@State private var isReordering: Bool = false
private let rowContent: (String) -> RowContent
init(titles: Binding<[String]>, @ViewBuilder rowContent: @escaping (String) -> RowContent = { (_: String) in EmptyView() })
{
self._titles = titles
self.rowContent = rowContent
}
var body: some View
{
List
{
ForEach(self.titles, id: \.self)
{
title in
self.rowContent(title).moveDisabled(!self.isReordering).swipeActions(allowsFullSwipe: false)
{
Button
{
self.editMode?.wrappedValue = .active
self.isReordering = true
}
label:
{
Label("Reorder", systemImage: "list.bullet")
}
}
}
.onMove
{
source, destination in self.titles.move(fromOffsets: source, toOffset: destination)
}
.contentShape(Rectangle())
.allowsHitTesting(self.isReordering)
.onTapGesture
{
if (self.isReordering)
{
withAnimation
{
self.isReordering = false
self.editMode?.wrappedValue = .inactive
}
}
}
}
}
}
If I have a row with a NavigationLink
or Text
it works fine. But when I have a row with a Button, .allowsHitTesting(self.isReordering) seems to be blocking the touches to it. If I comment that line out, it works fine, but then lists with NavigationLink
don't work. Easy solution is to send in a Bool
to TitleList
to let it know whether a button will be used or not, but I'm wondering if there is a better way to do this, because there's also a few other side effects of the way I'm doing it:
-To exit editingMode, the user can only tap the component in the row (i.e. the button or whatever) instead of being able to tap anywhere on the row (unless I wrap rowContent
in an HStack
with a Spacer()
).
-Even when I have .allowsHitTesting
set to false
for a Button
, I still have to disable it when in editingMode
or else when the user taps the Button, it will do its action instead of shutting off editingMode
.
So was wondering if anyone had a cleaner way to shut off editingMode that accounts for all possible UI components that can be in a row.
You don't need to use .allowsHitTesting
like that. To deal with the NavigationLink
acting up, you need to remove the button styling that it adds by default, by using .buttonStyle(.plain)
, although I am not sure it really applies in this case.
To prevent the Button
from triggering the action when in edit mode and to fix the NavigationLink
, the solution is a .highPriorityGesture
, that you enable conditionally based on the EditMode
state:
.highPriorityGesture(
TapGesture()
.onEnded {
if (self.isReordering) {
withAnimation {
self.isReordering = false
self.editMode?.wrappedValue = .inactive
}
}
}
, isEnabled: isReordering
)
import SwiftUI
struct ListHomeView: View {
@Environment(\.editMode) private var editMode
@State private var titles: [String] = ["Title 1", "Title 2", "Title 3"]
@ViewBuilder
var body: some View {
NavigationStack {
VStack {
Section("Buttons") {
TitleList(titles: $titles) { title in
Button {
print("Button Pushed")
} label : {
Text(title)
}
.buttonStyle(PlainButtonStyle())
}
}
Section("Navigation Links") {
TitleList(titles: self.$titles) { title in
NavigationLink(destination: Text("Test")) {
Text(title)
}
}
}
}
}
}
}
struct TitleList<RowContent: View>: View {
//Parameters
@Binding var titles: [String]
//Environment values
@Environment(\.editMode) private var editMode
@State private var isReordering: Bool = false
private let rowContent: (String) -> RowContent
//Initializer
init(titles: Binding<[String]>, @ViewBuilder rowContent: @escaping (String) -> RowContent = { (_: String) in EmptyView() })
{
self._titles = titles
self.rowContent = rowContent
}
//Body
var body: some View {
List {
ForEach(self.titles, id: \.self) { title in
rowContent(title)
.moveDisabled(!self.isReordering)
.swipeActions(allowsFullSwipe: false) {
Button {
self.editMode?.wrappedValue = .active
self.isReordering = true
} label: {
Label("Reorder", systemImage: "list.bullet")
}
}
}
.onMove {
source, destination in self.titles.move(fromOffsets: source, toOffset: destination)
}
.highPriorityGesture(
TapGesture()
.onEnded {
if (self.isReordering) {
withAnimation {
self.isReordering = false
self.editMode?.wrappedValue = .inactive
}
}
}
, isEnabled: isReordering
)
}
}
}
#Preview {
ListHomeView()
}
Note: By the way, not a fan of that coding structure. Please stick to conventional formatting when sharing or asking questions to make everyone lives easier.