4

How do you set an action to perform when the user hits the EditButton when it appears as "Done"? Is it even possible to do so?

Note that this is not the same as performing an action at each of the individual edits the user might do (like onDelete or onMove). How do you set an action to perform when the user is finished making all changes and ready to commit them?

It's necessary because I'm making all changes on a temporary copy of the model and will not commit the changes to the actual model until the user hits "Done". I am also providing a "Cancel" Button to discard the changes.

struct MyView: View {
    @Environment(\.editMode) var mode

    var body: some View {
        VStack {
            HStack {
                if self.mode?.value == .active {
                    Button(action: {
                        // discard changes here
                        self.mode?.value = .inactive
                    }) {
                        Text("Cancel")
                    }
                }

                Spacer()

                EditButton()
                // how to commit changes??
            }
            // controls to change the model
        }
    }
}

Is it even possible to set an action for "Done" on the EditButton or would I have to provide my own button to act like an EditButton? I could certainly do that, but it seems like it should be easy to do with the EditButton.

JJJ
  • 32,902
  • 20
  • 89
  • 102
salo.dm
  • 2,317
  • 1
  • 15
  • 16
  • I'm thinking you've debugged things very well - and found a limitation of the SwiftUI/Combine stack. Most apps *assume* that the user will accept that there's no "cancel" option as is (it's that way for the most part in UIKit also). I think you have two choices: use a model sheet duplicate of your list with a cancel, or "roll your own" edit functionality. If you want the latter, maybe this will help: https://stackoverflow.com/questions/57344305/swiftui-button-as-editbutton/57381570#57381570 –  Aug 22 '19 at 22:29
  • You could use an Undo Manager. There should be one in the environment. I haven't tried it myself but it's on my todo list. – Michael Salmon Aug 23 '19 at 05:21
  • @dfd , Michael Salmon: these are nice workarounds, within the spirit of SwifUI. But, I'm wondering if there's a way to directly attach an action to 'Done' on the EditButton. It seems like a pretty standard thing to do, but the documentation is still so skimpy there's no way to know. – salo.dm Aug 24 '19 at 01:53
  • You might be interested in this: https://stackoverflow.com/a/58323510/10614403 It's a way to preserve EditButton's functionality while still having the ability to customize. – Valentin Oct 10 '19 at 13:02

4 Answers4

12

New answer for XCode 12/iOS 14 using the onChange modifier.

This approach uses the SwiftUI editMode key path available via @Environment along with the new onChange modifier to determine when the view enters and exits editing mode.

In your view do the following:

@Environment(\.editMode) private var editMode

...

.onChange(of: editMode!.wrappedValue, perform: { value in 
  if value.isEditing {
     // Entering edit mode (e.g. 'Edit' tapped)
  } else {
     // Leaving edit mode (e.g. 'Done' tapped)
  }
})

Jason Armstrong
  • 1,058
  • 9
  • 17
3

You can use the onDisappear() modifier to perform the action, on a view that you show only on edit mode. There is an example on how to do it in the tutorial of SwiftUI "Working with UI Controls":

if self.mode?.wrappedValue == .inactive {
    ProfileSummary(profile: profile)
} else {
    ProfileEditor(profile: $draftProfile)
        .onDisappear {
            self.draftProfile = self.profile
        }
}

In your sample code above, since you do not have a view shown in edit mode, you can add state to check if you have clicked on "Cancel" or "Done" as below:

struct MyView: View {    
    @Environment(\.editMode) var mode
    @State var isCancelled = false

    var body: some View {
        VStack {
            HStack {
                if self.mode?.wrappedValue == .active {
                    Button(action: {
                            // discard changes here
                            self.mode?.wrappedValue = .inactive
                            self.isCancelled = true
                        }) {
                            Text("Cancel")
                        }
                        .onAppear() {
                            self.isCancelled = false
                        }
                        .onDisappear() {
                            if (self.isCancelled) {
                                print("Cancel")
                            } else {
                                print("Done")
                            }

                        }
                    }

                    Spacer()

                    EditButton()

                    // how to commit changes??
                }
                // controls to change the model
            }
        }
    }
szemian
  • 2,381
  • 3
  • 18
  • 22
  • This is a good workaround. But, it starts getting pretty clunky, adding flags and onAppear & onDisappear actions to multiple views. It kind of defeats the simplicity of SwiftUI. Is there a way to add an action directly to 'Done' on the EditButton? – salo.dm Aug 24 '19 at 01:40
  • It's precisely the tutorial you referred to that I'm trying to do. But, to do it as described, which is not what Apple's code actually does. The EditButton toggles between Edit & Done, not Edit & Cancel as the tutorial assumes. So the code Apple shows doesn't work as intended. – salo.dm Aug 24 '19 at 01:45
  • I agree it is kind of messy. From the little time I spent learning SwiftUI, it is mainly for View rather than ViewController than we are so used to. I still haven't dived into the Combine framework, maybe that is the better way to do it. – szemian Aug 24 '19 at 14:31
  • At first I thought it was odd to always save the model and have cancel revert to original values, but it has this real nice visual feedback when tapping cancel that all the edited fields can be seen to change back to their original values. Working with value types really does require thinking about things differently. – malhal Apr 13 '22 at 13:24
2

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.

MScottWaller
  • 3,321
  • 2
  • 24
  • 47
  • I think you may be right. This makes perfect sense, and then it follows that there should be no way to attach an action to the Done button. I'll leave the question open a bit longer to see if anyone comes up with a way to attach such an action, since it's more difficult to prove that there isn't one. I'll accept in a couple days if no one does. – salo.dm Aug 24 '19 at 20:41
  • I wonder if you could put a didChange on the Environment variable that shows whether the screen is in editMode. On the @Environment(\.editMode) var mode. If you could, you could trigger your code to commit changes there. – MScottWaller Aug 25 '19 at 00:39
  • It would still require a flag to determine which button changed the editMode, the Done or the Cancel. szemian's solution is about the simplest but in a larger program would get fairly clunky. Hence the search for a way to attach the action to the Done, if there is a way. – salo.dm Aug 25 '19 at 23:30
  • Yeah, if I remember right, that's more or less the way Apple does it in their tutorial. They bring up a form, and you edit it, and the actual changes are registered when you dismiss. – MScottWaller Aug 26 '19 at 14:46
  • If you want to completely separate actions for Edit, Done and Cancel, create them as separate buttons depending on the `editMode` – user1046037 Dec 18 '20 at 13:42
0

Your last comment contains a subtle change to things. Until then it sounded like you wanted both a "Done" and "Cancel" button - something very few iOS apps do.

But if you want a "Done" button with a "double-checking things, are you sure" you should be able to code such a thing by (possibly) manually going into edit mode and detecting when it's no longer in it. Save a "before" state, add a popup to when it is about to be changed, and you might give the user a rollback option.

FYI - I'm very old school. Not just PC, but mainframe. Not just Microsoft, but SAP. I certainly understand wanting to give the user one last chance before a destructive change - but not too many iOS apps do that. :-)

  • You were right in your original assessment; I do want both the Done & Cancel buttons. I'm actually trying to do Apple's tutorial: [https://developer.apple.com/tutorials/swiftui/working-with-ui-controls]. However, their own code doesn't do want they want. The tutorial says the EditButton provides a Cancel button, but what it actually provides is a Done Button. The tutorial asks to implement both Done & Cancel, and that's what I'm trying to do. Just trying to get a hold of SwiftUI – salo.dm Aug 24 '19 at 20:22
  • Im trying to do exactly the same thing mate! Have you Done (pun intended) any progress? – Mane Manero Sep 26 '19 at 09:58
  • @AlexandreLordelo, tl;dr - no. I wanted to (1) embed my list/table in a navigation bar, with (2) not just an Edit/Done button but an Add when not in edit mode, and (3) embed the whole thing as a smaller subview in my iPad app. That combination, which works so easily in `UIKit`, just had too many limitations in SwiftUI. In the end I opted to do it all in UIKit and use a `UIViewControllerRepresentable`. Wish I had better news. For you? Did you check the link in my answer? It might be of help.... –  Sep 26 '19 at 10:38
  • @salo.dm it's 2 years down the road, but the link you included in your comment includes the "]" and so the link is invalid. jic. – Zonker.in.Geneva Dec 26 '21 at 10:21
  • @Zonker.in.Geneva Thank you. You're right the link is invalid, but I can't edit the comment anymore. If you click on the link and then delete the final "]" from the url, you'll be directed to the correct page. – salo.dm Jan 04 '22 at 20:18