How to add a TextField to Alert in SwiftUI?

前端 未结 9 1943
温柔的废话
温柔的废话 2021-02-03 21:33

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

\"sample_image\"

9条回答
  •  梦如初夏
    2021-02-03 21:56

    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 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, isPresented: Binding?) {
        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?
    
      // 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? = nil
    
      // MARK: Modifiers
      func dismissable(_ isPresented: Binding) -> TextFieldAlert {
        TextFieldAlert(title: title, message: message, text: $text, isPresented: isPresented)
      }
    }
    
    extension TextFieldAlert: UIViewControllerRepresentable {
    
      typealias UIViewControllerType = TextFieldAlertViewController
    
      func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIViewControllerType {
        TextFieldAlertViewController(title: title, message: message, text: $text, isPresented: isPresented)
      }
    
      func updateUIViewController(_ uiViewController: UIViewControllerType,
                                  context: UIViewControllerRepresentableContext) {
        // 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: 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,
                          content: @escaping () -> TextFieldAlert) -> some View {
        TextFieldWrapper(isPresented: isPresented,
                         presentingView: self,
                         content: content)
      }
    }
    

提交回复
热议问题