Quelqu'un sait-il comment créer une alerte dans SwiftUI qui contient un champ de texte ?
Réponses
Trop de publicités?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 :
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!")
}
}
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)
}
}
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
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.
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.
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.
- Réponses précédentes
- Plus de réponses