12
Sep
2020
SwiftUI Alert with TextField
Reading time: 2 min
This recipe shows how to add a TextField
to a SwiftUI alert
dialog.
The end result looks like this:
The dialog supports:
- Custom title and message.
- Text field with a placeholder and keyboard type.
- Confirm button, whose callback returns the content of the text field.
- Optional secondary button and its callback.
- Optional cancel button.
Start by adding this struct to model the alert dialog:
public struct TextAlert {
public var title: String // Title of the dialog
public var message: String // Dialog message
public var placeholder: String = "" // Placeholder text for the TextField
public var accept: String = "OK" // The left-most button label
public var cancel: String? = "Cancel" // The optional cancel (right-most) button label
public var secondaryActionTitle: String? = nil // The optional center button label
public var keyboardType: UIKeyboardType = .default // Keyboard tzpe of the TextField
public var action: (String?) -> Void // Triggers when either of the two buttons closes the dialog
public var secondaryAction: (() -> Void)? = nil // Triggers when the optional center button is tapped
}
Next, add this extension that builds a UIAlertController
based on the model above:
extension UIAlertController {
convenience init(alert: TextAlert) {
self.init(title: alert.title, message: alert.message, preferredStyle: .alert)
addTextField {
$0.placeholder = alert.placeholder
$0.keyboardType = alert.keyboardType
}
if let cancel = alert.cancel {
addAction(UIAlertAction(title: cancel, style: .cancel) { _ in
alert.action(nil)
})
}
if let secondaryActionTitle = alert.secondaryActionTitle {
addAction(UIAlertAction(title: secondaryActionTitle, style: .default, handler: { _ in
alert.secondaryAction?()
}))
}
let textField = self.textFields?.first
addAction(UIAlertAction(title: alert.accept, style: .default) { _ in
alert.action(textField?.text)
})
}
}
Now it's time to make use of UIViewControllerRepresentable
to render the alert dialog in the hosting view controller:
struct AlertWrapper<Content: View>: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let alert: TextAlert
let content: Content
func makeUIViewController(context: UIViewControllerRepresentableContext<AlertWrapper>) -> UIHostingController<Content> {
UIHostingController(rootView: content)
}
final class Coordinator {
var alertController: UIAlertController?
init(_ controller: UIAlertController? = nil) {
self.alertController = controller
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: UIViewControllerRepresentableContext<AlertWrapper>) {
uiViewController.rootView = content
if isPresented && uiViewController.presentedViewController == nil {
var alert = self.alert
alert.action = {
self.isPresented = false
self.alert.action($0)
}
context.coordinator.alertController = UIAlertController(alert: alert)
uiViewController.present(context.coordinator.alertController!, animated: true)
}
if !isPresented && uiViewController.presentedViewController == context.coordinator.alertController {
uiViewController.dismiss(animated: true)
}
}
}
Finally, add this covenient extension that allows you to show a TextAlert on any SwiftUI View
, just like you would with a regular alert
:
extension View {
public func alert(isPresented: Binding<Bool>, _ alert: TextAlert) -> some View {
AlertWrapper(isPresented: isPresented, alert: alert, content: self)
}
}
Now you can use the TextAlert extension on any view:
@State private var showDialog = false
var body: some View {
VStack {
Text("Some text")
}.alert(isPresented: $showDialog,
TextAlert(title: "Title",
message: "Message",
keyboardType: .numberPad) { result in
if let text = result) {
// Text was accepted
} else {
// The dialog was cancelled
}
})
}