How to add a TextField to Alert in SwiftUI?

Alert is quite limited at the moment, but you can roll your own solution in pure SwiftUI.

Here's a simple implementation of a custom alert with a text field.

struct TextFieldAlert<Presenting>: View where Presenting: View {

    @Binding var isShowing: Bool
    @Binding var text: String
    let presenting: Presenting
    let title: String

    var body: some View {
        GeometryReader { (deviceSize: GeometryProxy) in
            ZStack {
                self.presenting
                    .disabled(isShowing)
                VStack {
                    Text(self.title)
                    TextField(self.$text)
                    Divider()
                    HStack {
                        Button(action: {
                            withAnimation {
                                self.isShowing.toggle()
                            }
                        }) {
                            Text("Dismiss")
                        }
                    }
                }
                .padding()
                .background(Color.white)
                .frame(
                    width: deviceSize.size.width*0.7,
                    height: deviceSize.size.height*0.7
                )
                .shadow(radius: 1)
                .opacity(self.isShowing ? 1 : 0)
            }
        }
    }

}

And a View extension to use it:

extension View {

    func textFieldAlert(isShowing: Binding<Bool>,
                        text: Binding<String>,
                        title: String) -> some View {
        TextFieldAlert(isShowing: isShowing,
                       text: text,
                       presenting: self,
                       title: title)
    }

}

Demo:

enter image description here

struct ContentView : View {

    @State private var isShowingAlert = false
    @State private var alertInput = ""

    var body: some View {
        NavigationView {
            VStack {
                Button(action: {
                    withAnimation {
                        self.isShowingAlert.toggle()
                    }
                }) {
                    Text("Show alert")
                }
            }
            .navigationBarTitle(Text("A List"), displayMode: .large)
        }
        .textFieldAlert(isShowing: $isShowingAlert, text: $alertInput, title: "Alert!")
    }
}

As the Alert view provided by SwiftUI doesn't do the job you will need indeed to use UIAlertController from UIKit. Ideally we want a TextFieldAlert view that we can presented in the same way we would present the Alert provided by SwiftUI:

struct MyView: View {

  @Binding var alertIsPresented: Bool
  @Binding var text: String? // this is updated as the user types in the text field

  var body: some View {
    Text("My Demo View")
      .textFieldAlert(isPresented: $alertIsPresented) { () -> TextFieldAlert in
        TextFieldAlert(title: "Alert Title", message: "Alert Message", text: self.$text)
    }
  }
}

We can achieve this writing a couple of classes and adding a modifier in a View extension.

1) TextFieldAlertViewController creates a UIAlertController (with a text field of course) and presents it when it appears on screen. User changes to the text field are reflected into a Binding<String> that is passed during initializazion.

class TextFieldAlertViewController: UIViewController {

  /// Presents a UIAlertController (alert style) with a UITextField and a `Done` button
  /// - Parameters:
  ///   - title: to be used as title of the UIAlertController
  ///   - message: to be used as optional message of the UIAlertController
  ///   - text: binding for the text typed into the UITextField
  ///   - isPresented: binding to be set to false when the alert is dismissed (`Done` button tapped)
  init(title: String, message: String?, text: Binding<String?>, isPresented: Binding<Bool>?) {
    self.alertTitle = title
    self.message = message
    self._text = text
    self.isPresented = isPresented
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // MARK: - Dependencies
  private let alertTitle: String
  private let message: String?
  @Binding private var text: String?
  private var isPresented: Binding<Bool>?

  // MARK: - Private Properties
  private var subscription: AnyCancellable?

  // MARK: - Lifecycle
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    presentAlertController()
  }

  private func presentAlertController() {
    guard subscription == nil else { return } // present only once

    let vc = UIAlertController(title: alertTitle, message: message, preferredStyle: .alert)

    // add a textField and create a subscription to update the `text` binding
    vc.addTextField { [weak self] textField in
      guard let self = self else { return }
      self.subscription = NotificationCenter.default
        .publisher(for: UITextField.textDidChangeNotification, object: textField)
        .map { ($0.object as? UITextField)?.text }
        .assign(to: \.text, on: self)
    }

    // create a `Done` action that updates the `isPresented` binding when tapped
    // this is just for Demo only but we should really inject
    // an array of buttons (with their title, style and tap handler)
    let action = UIAlertAction(title: "Done", style: .default) { [weak self] _ in
      self?.isPresented?.wrappedValue = false
    }
    vc.addAction(action)
    present(vc, animated: true, completion: nil)
  }
}

2) TextFieldAlert wraps TextFieldAlertViewController using the UIViewControllerRepresentable protocol so that it can be used within SwiftUI.

struct TextFieldAlert {

  // MARK: Properties
  let title: String
  let message: String?
  @Binding var text: String?
  var isPresented: Binding<Bool>? = nil

  // MARK: Modifiers
  func dismissable(_ isPresented: Binding<Bool>) -> TextFieldAlert {
    TextFieldAlert(title: title, message: message, text: $text, isPresented: isPresented)
  }
}

extension TextFieldAlert: UIViewControllerRepresentable {

  typealias UIViewControllerType = TextFieldAlertViewController

  func makeUIViewController(context: UIViewControllerRepresentableContext<TextFieldAlert>) -> UIViewControllerType {
    TextFieldAlertViewController(title: title, message: message, text: $text, isPresented: isPresented)
  }

  func updateUIViewController(_ uiViewController: UIViewControllerType,
                              context: UIViewControllerRepresentableContext<TextFieldAlert>) {
    // no update needed
  }
}

3) TextFieldWrapper is a simple ZStack with a TextFieldAlert on the back (only if isPresented is true) and a presenting view on the front. The presenting view is the only one visibile.

struct TextFieldWrapper<PresentingView: View>: View {

  @Binding var isPresented: Bool
  let presentingView: PresentingView
  let content: () -> TextFieldAlert

  var body: some View {
    ZStack {
      if (isPresented) { content().dismissable($isPresented) }
      presentingView
    }
  }  
}

4) The textFieldAlert modifier allows us to smoothly wrap any SwiftUI view in a TextFieldWrapper and obtain the desired behaviour.

extension View {
  func textFieldAlert(isPresented: Binding<Bool>,
                      content: @escaping () -> TextFieldAlert) -> some View {
    TextFieldWrapper(isPresented: isPresented,
                     presentingView: self,
                     content: content)
  }
}

I found modals and alerts in SwiftUI to be lacking several features. For instance, there doesn't seem to be a way of presenting a modal with FormSheet style.

When I need to present a complex alert (such as one with textfields), I create a pure SwiftUI view with all the content of the alert, and then present it as a FormSheet using a UIHostController.

If you do not have a UIViewController around to call present(), you can always use the root view controller.

With this approach you get some nice features, such as the standard alert animation both going in, and out. You can also drag the alert down to dismiss it.

The alert view also moves up when the keyboard appears.

This works nicely on iPad. On iPhone, FormSheet is full screen so you may need to tweak the code to find a solution. I think this will give you a good starting point.

enter image description here

It is something like this:

struct ContentView : View {
    @State private var showAlert = false

    var body: some View {
        VStack {
            Button(action: {
                let alertHC = UIHostingController(rootView: MyAlert())

                alertHC.preferredContentSize = CGSize(width: 300, height: 200)
                alertHC.modalPresentationStyle = UIModalPresentationStyle.formSheet

                UIApplication.shared.windows[0].rootViewController?.present(alertHC, animated: true)

            }) {
                Text("Show Alert")
            }
        }
    }
}

struct MyAlert: View {
    @State private var text: String = ""

    var body: some View {

        VStack {
            Text("Enter Input").font(.headline).padding()

            TextField($text, placeholder: Text("Type text here")).textFieldStyle(.roundedBorder).padding()
            Divider()
            HStack {
                Spacer()
                Button(action: {
                    UIApplication.shared.windows[0].rootViewController?.dismiss(animated: true, completion: {})
                }) {

                    Text("Done")
                }
                Spacer()

                Divider()

                Spacer()
                Button(action: {
                    UIApplication.shared.windows[0].rootViewController?.dismiss(animated: true, completion: {})
                }) {
                    Text("Cancel")
                }
                Spacer()
            }.padding(0)


            }.background(Color(white: 0.9))
    }
}

If you find yourself using this a lot, the button row may be encapsulated in a separate view for easy reuse.