Anyone an idea how to create an Alert in SwiftUI that contains a TextField?
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.
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:
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) } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With