iosswiftswiftuimobile-development

SwiftUI popup alert, button action not working the first time


What I want to achieve: when clicking the save button, it pops up a box asking for the name. after user gives a name and hits the confirm button, it performs some actions and dismisses the view (return to the parent view).

here's my code, however, it doesn't perform any actions when I save and confirm the first time. All following attempts works. any idea why it doesn't work for the first time?

    Button(action: {
      self.isShowingNameInput = true
    }) {
      Text("SAVE")
    }
    .alert("Task Name", isPresented: self.$isShowingNameInput, actions: {
      TextField("", text: self.$options.name)  

      Button(action: {
        log("Saved \(self.options.name)")
        self.presentationMode.wrappedValue.dismiss()
      }) {
        Text("Confirm")
      }.disabled(self.options.name.isEmpty)

      Button(role: .cancel, action: {}) {
        Text("Cancel")
      }
    }, message: {})

Solution

  • I am not sure if you can have a TextField in the actions of an alert. In general, the data for an alert should not change after presentation occurs and this also applies to the values of state variables that you show in the content of the alert. See Update text inside alert message which relates to a similar issue.

    As a workaround, you could consider showing a sheet instead:

    struct ConfirmationDialog: View {
        @Binding var options: Options
        @Environment(\.dismiss) private var dismiss
    
        var body: some View {
            VStack {
                Spacer()
                Text("Please enter your name")
                TextField("Name", text: $options.name)
                    .textFieldStyle(.roundedBorder)
                Spacer()
                Button {
                    print("Saved \(options.name)")
                    dismiss()
                } label: {
                    Text("Confirm")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)
                .disabled(options.name.isEmpty)
    
                Button(role: .cancel) {
                    dismiss()
                } label: {
                    Text("Cancel")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.bordered)
            }
            .padding()
        }
    }
    
    struct ContentView: View {
        @State private var isShowingNameInput = false
        @State private var options = Options()
    
        var body: some View {
            Button("SAVE") {
                isShowingNameInput = true
            }
            .sheet(isPresented: $isShowingNameInput) {
                ConfirmationDialog(options: $options)
                    .presentationDetents([.height(250)])
            }
        }
    }
    

    Animation

    Alternatively, you could implement a custom alert. The answer to the post mentioned above shows an example of how this can be done (it was my answer).

    EDIT Ref. the questions in your comment: the reason for implementing the dialog as a separate View is so that dismiss can be used to dismiss the sheet, which I thought was what you were trying to do before. See the documentation for more details about dismiss. However, you don't have to implement it this way - if the sheet would be contained in the view that shows it then it could be dismisssed just by setting the state variable isShowingNameInput back to false. The dialog buttons could then call the dismiss action associated with the containing view instead, in order to go back to the parent view. There was no parent view in your example though.

    EDIT2 To have the text field auto-focus, try adding a FocusState variable which you then set to true in .onAppear:

    @FocusState private var isFocused: Bool
    
    TextField("Name", text: $options.name)
        .textFieldStyle(.roundedBorder)
        .focused($isFocused)
        .onAppear { isFocused = true }