Reputation: 329
Trying to have a Form
with multiple sections and each Section
with it's own EditButton
.
How to trigger a Section
into "edit mode" without triggering all sections in the Form
, as seen in the attached gif.
How to track if the EditButton
in a certain Section
is triggered so that a Button
appears in that Section
.
I used code from these two sources: developer.apple.com, stackoverflow.com
Here is the code:
import SwiftUI
struct ContentView: View {
@Environment(\.editMode) private var editMode
@State private var section1: [String] = ["Item 1", "Item 2"]
@State private var section2: [String] = ["Item 3", "Item 4"]
@State private var isEditingSection1 = false
@State private var isEditingSection2 = false
var body: some View {
Form {
// Section 1
Section (header:
EditButton().frame(maxWidth: .infinity, alignment: .trailing)
.overlay(
HStack {
Image(systemName: "folder")
.foregroundColor(Color.gray)
Text("Section 1")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section1, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection1)
.onMove(perform: moveSection1)
// Add item option
if editMode?.wrappedValue.isEditing ?? true /*isEditingSection1*/ {
Button ("Add Item") {
// add action
}
}
}
// Section 2
Section (header:
EditButton().frame(maxWidth: .infinity, alignment: .trailing)
.overlay(
HStack {
Image(systemName: "tray")
.foregroundColor(Color.gray)
Text("Section 2")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section2, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection2)
.onMove(perform: moveSection2)
// Add item option
if editMode?.wrappedValue.isEditing ?? true /*isEditingSection2*/ {
Button ("Add Item") {
// add action
}
}
}
}
}
func deleteSection1(at offsets: IndexSet) {
section1.remove(atOffsets: offsets)
}
func moveSection1(from source: IndexSet, to destination: Int) {
section1.move(fromOffsets: source, toOffset: destination)
}
func deleteSection2(at offsets: IndexSet) {
section2.remove(atOffsets: offsets)
}
func moveSection2(from source: IndexSet, to destination: Int) {
section2.move(fromOffsets: source, toOffset: destination)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Upvotes: 5
Views: 1075
Reputation: 31
Update for iOS 16:
I wish I knew the why, but in iOS 16 you have to phrase the boolean expression like this.
.deleteDisabled(isEditingSection2)
.deleteDisabled((isEditingSection2 || isEditingSection3))
// Etc...
In iOS 15.5 you can get away with either.
.deleteDisabled(!isEditingSection1)
// Or...
.deleteDisabled(isEditingSection2)
Here's a complete ContentView
and EditButton
showing this. I added a third section to help demo how the boolean OR logic should be applied inside .deleteDisabled
and .moveDisabled
.
import SwiftUI
struct ContentView: View {
@State private var section1: [String] = ["Item 1", "Item 2"]
@State private var section2: [String] = ["Item 3", "Item 4"]
@State private var section3: [String] = ["Item 5", "Item 6"]
@State private var isEditingSection1 = false
@State private var isEditingSection2 = false
@State private var isEditingSection3 = false
private var isEditingOn: Bool { //<=== Here
isEditingSection1 || isEditingSection2 || isEditingSection3
}
var body: some View {
Form {
// Section 1
Section (header:
EditButton(isEditing: $isEditingSection1, printBools: printBools)
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(
HStack {
Image(systemName: "folder")
.foregroundColor(Color.gray)
Text("Section 1")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section1, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection1)
.onMove(perform: moveSection1)
.moveDisabled(isEditingSection2 || isEditingSection3) //<=== Here
.deleteDisabled((isEditingSection2 || isEditingSection3)) //<=== Here
// BIG NOTE!!
// This works in iOS 15.5, but not iOS 16: `.deleteDisabled(!isEditingSection1)`
// This works in both 15.5 and 16.0. Why??? `.deleteDisabled(isEditingSection2 || isEditingSection3)`
// Add item option
if isEditingSection1 { //<=== Here
Button ("Add Item") {
// add action
}
}
}
// Section 2
Section(header:
EditButton(isEditing: $isEditingSection2, printBools: printBools)
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(
HStack {
Image(systemName: "tray")
.foregroundColor(Color.gray)
Text("Section 2")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section2, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection2)
.onMove(perform: moveSection2)
.moveDisabled(isEditingSection1 || isEditingSection3) //<=== Here
.deleteDisabled(isEditingSection1 || isEditingSection3) //<=== Here
// Add item option
if isEditingSection2 { //<=== Here
Button ("Add Item") {
// add action
}
}
}
// Section 3
Section(header:
EditButton(isEditing: $isEditingSection3, printBools: printBools)
.frame(maxWidth: .infinity, alignment: .trailing)
.overlay(
HStack {
Image(systemName: "tray")
.foregroundColor(Color.gray)
Text("Section 3")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section3, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection3)
.onMove(perform: moveSection3)
.moveDisabled(isEditingSection1 || isEditingSection2) //<=== Here
.deleteDisabled(isEditingSection1 || isEditingSection2) //<=== Here
// Add item option
if isEditingSection3 { //<=== Here
Button ("Add Item") {
// add action
}
}
}
}.environment(\.editMode, isEditingOn ? .constant(.active) : .constant(.inactive)) //<=== Here
}
func deleteSection1(at offsets: IndexSet) {
section1.remove(atOffsets: offsets)
}
func moveSection1(from source: IndexSet, to destination: Int) {
section1.move(fromOffsets: source, toOffset: destination)
}
func deleteSection2(at offsets: IndexSet) {
section2.remove(atOffsets: offsets)
}
func moveSection2(from source: IndexSet, to destination: Int) {
section2.move(fromOffsets: source, toOffset: destination)
}
func deleteSection3(at offsets: IndexSet) {
section3.remove(atOffsets: offsets)
}
func moveSection3(from source: IndexSet, to destination: Int) {
section3.move(fromOffsets: source, toOffset: destination)
}
func printBools() {
// let _ = print("isEditingSection1 = \(isEditingSection1)")
// let _ = print("isEditingSection2 = \(isEditingSection2)")
}
}
struct EditButton: View {
@Binding var isEditing: Bool
let printBools: () -> Void
var body: some View {
Button(isEditing ? "DONE" : "EDIT") {
printBools()
withAnimation {
isEditing.toggle()
}
printBools()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Note that logically here, both expressions !isEditingSection1
and isEditingSection2 || isEditingSection3
are equivalent when we consider how we are using them here.
isEditingSection1 | ! isEditingSection1 | isEditingSection2 OR isEditingSection3 |
---|---|---|
true |
false |
false |
false |
true |
true , if editing another section. Otherwise false . |
Upvotes: 3
Reputation: 18924
There is no inbuilt thing for set different editing mode for each section.
But you can use it explicitly to set editing mode and disable/enable delete and move action for each row.
Here is the possible solution demo.
For this, you need to first create your own EditButton with a binding bool value.
struct EditButton: View {
@Binding var isEditing: Bool
var body: some View {
Button(isEditing ? "DONE" : "EDIT") {
withAnimation {
isEditing.toggle()
}
}
}
}
Now your Form
view is.
struct ContentViewEditModeDemo: View {
@State private var section1: [String] = ["Item 1", "Item 2"]
@State private var section2: [String] = ["Item 3", "Item 4"]
@State private var isEditingSection1 = false
@State private var isEditingSection2 = false
private var isEditingOn: Bool { //<=== Here
isEditingSection1 || isEditingSection2
}
var body: some View {
Form {
// Section 1
Section (header:
EditButton(isEditing: $isEditingSection1).frame(maxWidth: .infinity, alignment: .trailing) //<=== Here
.overlay(
HStack {
Image(systemName: "folder")
.foregroundColor(Color.gray)
Text("Section 1")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section1, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection1)
.onMove(perform: moveSection1)
.moveDisabled(!isEditingSection1) //<=== Here
.deleteDisabled(!isEditingSection1) //<=== Here
// Add item option
if isEditingSection1 { //<=== Here
Button ("Add Item") {
// add action
}
}
}
// Section 2
Section(header:
EditButton(isEditing: $isEditingSection2).frame(maxWidth: .infinity, alignment: .trailing) //<=== Here
.overlay(
HStack {
Image(systemName: "tray")
.foregroundColor(Color.gray)
Text("Section 2")
.textCase(.none)
.foregroundColor(Color.gray)
}, alignment: .leading)
.foregroundColor(.blue)) {
ForEach(section2, id: \.self) { item in
Text(item)
}
.onDelete(perform: deleteSection1)
.onMove(perform: moveSection1)
.moveDisabled(!isEditingSection2) //<=== Here
.deleteDisabled(!isEditingSection2) //<=== Here
// Add item option
if isEditingSection2 { //<=== Here
Button ("Add Item") {
// add action
}
}
}
}.environment(\.editMode, isEditingOn ? .constant(.active) : .constant(.inactive)) //<=== Here
}
func deleteSection1(at offsets: IndexSet) {
section1.remove(atOffsets: offsets)
}
func moveSection1(from source: IndexSet, to destination: Int) {
section1.move(fromOffsets: source, toOffset: destination)
}
func deleteSection2(at offsets: IndexSet) {
section2.remove(atOffsets: offsets)
}
func moveSection2(from source: IndexSet, to destination: Int) {
section2.move(fromOffsets: source, toOffset: destination)
}
}
Upvotes: 1