SwiftUI EditButton action on Done
From what I understand, the EditButton is meant to put the entire environment in edit mode. That means going into a mode where, for example, you rearrange or delete items in a list. Or where text fields become editable. Those sorts of things. "Done" means "I'm done being in edit mode", not "I want to apply my changes." You would want a different "Apply changes," "Save," "Confirm" or whatever button to do those things.
SwiftUI how to perform action when EditMode changes?
UPDATED for iOS 15.
This solution catches 2 birds with one stone:
- The entire view redraws itself when editMode is toggle
- A specific action can be performed upon activation/inactivation of editMode
Hopes this helps someone else.
struct ContentView: View {
@State var editMode: EditMode = .inactive
@State var selection = Set<UUID>()
@State var items = [Item(), Item(), Item()]
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(items) { item in
Text(item.title)
}
}
.navigationTitle(Text("Demo"))
.environment(\.editMode, self.$editMode)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
editButton
}
ToolbarItem(placement: .navigationBarTrailing) {
addDelButton
}
}
}
}
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}
private var addDelButton: some View {
if editMode == .inactive {
return Button(action: addItem) {
Image(systemName: "plus")
}
} else {
return Button(action: deleteItems) {
Image(systemName: "trash")
}
}
}
private func addItem() {
items.append(Item())
}
private func deleteItems() {
for id in selection {
if let index = items.lastIndex(where: { $0.id == id }) {
items.remove(at: index)
}
}
selection = Set<UUID>()
}
}
extension EditMode {
var title: String {
self == .active ? "Done" : "Edit"
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}
SwiftUI: EditButton says Done after swipe to delete action
It seems EditButton
is incompatible with this new way of using @StateObject
to declare the store object. I had the idea to try reverting to the old way of creating the store object and surprisingly it fixed the issue. Change the ScrumStore
to this
class ScrumStore: ObservableObject {
@Published var scrums: [DailyScrum] = []
static var shared = ScrumStore()
And change the ScrumdingerApp
to this:
struct ScrumdingerApp: App {
private var store = ScrumStore.shared
var body: some Scene {
WindowGroup {
NavigationView {
ScrumsView(store: store) {
And ScrumsView
to this:
struct ScrumsView: View {
//@Binding var scrums: [DailyScrum]
@ObservedObject var store: ScrumStore
...
var body: some View {
List {
ForEach($store.scrums) { $scrum in
And change the other scrums
to store.scrums
too.
This makes the edit button work properly for swipe to delete
I would also improve the code by making the store object be an Environment object, e.g.
struct ScrumdingerApp: App {
private var store = ScrumStore.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(store)
}
}
struct ContentView: View {
@EnvironmentObject var store: ScrumStore
...
I would also improve how the model structs are loaded and saved, e.g. I would load it in the object's init and have a function to save it. The function could be called on a scene phase or after the model is mutated. Before doing too much loading and saving you might want to try FileDocument
which does all that for you and gives you iCloud drive support for free. I recommend the document app WWDC videos to learn that, there is a new one every year.
SwiftUI Button as EditButton
The implementation below replaces EditButton
's functionality with a Button
:
import SwiftUI
struct ContentView: View {
@State var isEditing = false
@State var selection = Set<String>()
var names = ["Karl", "Hans", "Faustao"]
var body: some View {
NavigationView {
VStack {
List(names, id: \.self, selection: $selection) { name in
Text(name)
}
.navigationBarTitle("Names")
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
Button(action: {
self.isEditing.toggle()
}) {
Text(isEditing ? "Done" : "Edit")
.frame(width: 80, height: 40)
}
.background(Color.yellow)
}
.padding(.bottom)
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Result
However, by doing so, selection handling needs to be implemented by our own (which may or may not be an issue).
Unfortunately there isn't much documentation around that at this point:
https://developer.apple.com/documentation/swiftui/list/3367016-init
SwiftUI detect edit mode
It is on same level so environment is not visible, because it is activated for sub-views.
A possible solution is to separate dependent part into standalone view, like
Form {
InternalView()
}
.toolbar {
EditButton()
}
Tested with Xcode 13.4 / iOS 15.5
Test module on GitHub
SwiftUI EditButton in HStack not activating edit mode
Here is working solution - looks like they require that EditButton was a root view of section, so we can construct everything else above it. (tested with Xcode 11.4 / iOS 13.4)
Note: @Environment(\.editMode) var editMode
is not needed
Section(header:
EditButton().frame(maxWidth: .infinity, alignment: .trailing)
.overlay(Text("Header"), alignment: .leading)
)
{
ForEach(items, id: \.self) { item in
Text(item)
}
.onMove(perform: reorderItems)
.onDelete(perform: deleteItems)
}
How to check EditButton/EditMode state in SwiftUI
You can achieve this by setting .environment
with @State
mode.
struct TestView: View {
@State private var list = [1,2,3,4,5]
@State private var selection = Set<Int>()
@State var mode: EditMode = .inactive //< -- Here
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(list, id: \.self) { item in
Text("\(item)")
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
ToolbarItem(placement: .navigationBarTrailing) {
Text(mode == .active ? "Editing" : "Not Editing")
}
}.environment(\.editMode, $mode) //< -- Here
}
}
}
SwiftUI Form with Multiple EditButtons
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)
}
}
Related Topics
Using Uilexicon to Implement Autocorrect in iOS 8 Keyboard Extension
Swift Change the Tableviewcell Border Color According to Data
How to Change Text Color of Actionsheet in Swiftui
Avoid Automatic Framework Linking in Swift
Xcode 11 - Disable Resize Mode in Catalyst Swift
Toggle Sidebar in Swiftui Navigationview on MACos
Swift Build' on Terminal Throw 'Error: Root Manifest Not Found'
In Swift, How to Extend a Typealias
Get the Type of Anyobject Dynamically in Swift
How to Conform an Observableobject to the Codable Protocols
Is There a Github Markdown Language Identifier for Swift Code
Zoom to Fit Current Location and Annotation on Map
Compile Time Key Path Checking in Swift
Swift Struct with Lazy, Private Property Conforming to Protocol