Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add a TextField to Alert in SwiftUI?

Anyone an idea how to create an Alert in SwiftUI that contains a TextField?

sample_image

like image 782
Captain Vril Avatar asked Jun 23 '19 18:06

Captain Vril


People also ask

How do you make a TextField first responder in SwiftUI?

Add focused(_:equals:) modifier to have a binding value, equal to the enum cases. Now, you can change the focusedField to whichever textfield you want to have cursor on, or resign first responder by assigning nil to focusedField.


2 Answers

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!")     } } 
like image 60
Matteo Pacini Avatar answered Sep 25 '22 08:09

Matteo Pacini


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)   } } 
like image 33
tanzolone Avatar answered Sep 25 '22 08:09

tanzolone