77 votes

Comment ajouter un champ de texte à une alerte dans SwiftUI ?

Quelqu'un sait-il comment créer une alerte dans SwiftUI qui contient un champ de texte ?

sample_image

35voto

Matteo Pacini Points 2704

Alert est assez limitée pour le moment, mais vous pouvez mettre en place votre propre solution dans le pur SwiftUI.

Voici une mise en œuvre simple d'une alerte personnalisée avec un champ de texte.

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)
            }
        }
    }

}

Et un View pour l'utiliser :

extension View {

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

}

Démo :

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!")
    }
}

27voto

tanzolone Points 2090

Comme le Alert vue fournie par SwiftUI ne fait pas l'affaire, vous devrez en effet utiliser UIAlertController de UIKit . Idéalement, nous voulons un TextFieldAlert que nous pouvons présenter de la même manière que nous présenterions le Alert fourni par 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)
    }
  }
}

Nous pouvons réaliser cela en écrivant quelques classes et en ajoutant un modificateur dans un fichier View extension.

1) TextFieldAlertViewController crée un UIAlertController (avec un champ de texte bien sûr) et le présente lorsqu'il apparaît à l'écran. Les modifications apportées par l'utilisateur au champ de texte sont reflétées dans un fichier Binding<String> qui est passé lors de l'initialisation.

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 enveloppes TextFieldAlertViewController en utilisant le UIViewControllerRepresentable afin qu'il puisse être utilisé dans 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 est un simple ZStack avec un TextFieldAlert au dos (uniquement si isPresented est vrai) et une vue de présentation sur le devant. La vue de présentation est la seule visible.

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) Le textFieldAlert nous permet d'envelopper en douceur n'importe quelle vue SwiftUI dans une vue TextFieldWrapper et obtenir le comportement souhaité.

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

16voto

Fabian Streitel Points 918

Vous pouvez simplement utiliser UIAlertController directement. Il n'est pas nécessaire de créer votre propre interface utilisateur de dialogue d'alerte :

private func alert() {
    let alert = UIAlertController(title: "title", message: "message", preferredStyle: .alert)
    alert.addTextField() { textField in
        textField.placeholder = "Enter some text"
    }
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in })
    showAlert(alert: alert)
}

func showAlert(alert: UIAlertController) {
    if let controller = topMostViewController() {
        controller.present(alert, animated: true)
    }
}

private func keyWindow() -> UIWindow? {
    return UIApplication.shared.connectedScenes
    .filter {$0.activationState == .foregroundActive}
    .compactMap {$0 as? UIWindowScene}
    .first?.windows.filter {$0.isKeyWindow}.first
}

private func topMostViewController() -> UIViewController? {
    guard let rootController = keyWindow()?.rootViewController else {
        return nil
    }
    return topMostViewController(for: rootController)
}

private func topMostViewController(for controller: UIViewController) -> UIViewController {
    if let presentedController = controller.presentedViewController {
        return topMostViewController(for: presentedController)
    } else if let navigationController = controller as? UINavigationController {
        guard let topController = navigationController.topViewController else {
            return navigationController
        }
        return topMostViewController(for: topController)
    } else if let tabController = controller as? UITabBarController {
        guard let topController = tabController.selectedViewController else {
            return tabController
        }
        return topMostViewController(for: topController)
    }
    return controller
}

La majeure partie de ce code n'est qu'un modèle pour trouver le ViewController qui doit présenter l'alerte. Appeler alert() par exemple, de la action d'un bouton :

struct TestView: View {
    var body: some View {
        Button(action: { alert() }) { Text("click me") }
     }
}

Attention cependant, il semble qu'il y ait un bogue dans la version 5 et les suivantes qui peut parfois provoquer le blocage de l'émulateur dès qu'un champ de texte est affiché : Xcode 11 beta 5 : L'interface se fige lors de l'ajout de textFields dans UIAlertController

12voto

kontiki Points 633

J'ai trouvé que les modales et les alertes dans SwiftUI manquaient de plusieurs fonctionnalités. Par exemple, il ne semble pas y avoir de moyen de présenter une modale avec un style FormSheet.

Lorsque j'ai besoin de présenter une alerte complexe (telle qu'une alerte avec des champs de texte), je crée une vue SwiftUI pure avec tout le contenu de l'alerte, et je la présente ensuite comme une FormSheet en utilisant une balise UIHostController .

Si vous ne disposez pas d'un UIViewController pour appeler present(), vous pouvez toujours utiliser le contrôleur de vue Root.

Cette approche permet d'obtenir des fonctionnalités intéressantes, telles que l'animation standard des alertes à l'entrée et à la sortie. Vous pouvez également faire glisser l'alerte vers le bas pour la faire disparaître.

La vue des alertes se déplace également vers le haut lorsque le clavier apparaît.

Cela fonctionne très bien sur l'iPad. Sur l'iPhone, FormSheet est en plein écran, vous devrez donc peut-être modifier le code pour trouver une solution. Je pense que cela vous donnera un bon point de départ.

enter image description here

C'est quelque chose comme ça :

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))
    }
}

Si vous vous en servez souvent, la rangée de boutons peut être encapsulée dans une vue distincte pour faciliter sa réutilisation.

6voto

Senseful Points 11193

Bien que ce ne soit pas exactement la même chose, si tout ce que vous recherchez est une vue native, de type modale, avec une boîte d'édition, vous pouvez utiliser un fichier de type popover . Il fonctionne dès la sortie de la boîte (moins une problème de dimensionnement ) sans avoir à parcourir la hiérarchie des vues.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X