Anyone an idea how to create an Alert in SwiftUI that contains a TextField?
This is an example based on the SwiftUI Sheet
class that displays a dialog with a prompt, a text field, and the classic OK and Dismiss button
First lets make our Dialog
class, which will pop when user want to edit a value:
import SwiftUI
struct Dialog: View {
@Environment(\.presentationMode) var presentationMode
/// Edited value, passed from outside
@Binding var value: String?
/// Prompt message
var prompt: String = ""
/// The value currently edited
@State var fieldValue: String
/// Init the Dialog view
/// Passed @binding value is duplicated to @state value while editing
init(prompt: String, value: Binding<String?>) {
_value = value
self.prompt = prompt
_fieldValue = State<String>(initialValue: value.wrappedValue ?? "")
}
var body: some View {
VStack {
Text(prompt).padding()
TextField("", text: $fieldValue)
.frame(width: 200, alignment: .center)
HStack {
Button("OK") {
self.value = fieldValue
self.presentationMode.wrappedValue.dismiss()
}
Button("Dismiss") {
self.presentationMode.wrappedValue.dismiss()
}
}.padding()
}
.padding()
}
}
#if DEBUG
struct Dialog_Previews: PreviewProvider {
static var previews: some View {
var name = "John Doe"
Dialog(prompt: "Name", value: Binding<String?>.init(get: { name }, set: {name = $0 ?? ""}))
}
}
#endif
Now we use it this way in the caller View:
import SwiftUI
struct ContentView: View {
/// Is the input dialog displayed
@State var dialogDisplayed = false
/// The name to edit
@State var name: String? = nil
var body: some View {
VStack {
Text(name ?? "Unnamed").frame(width: 200).padding()
Button(name == nil ? "Set Name" : "Change Name") {
dialogDisplayed = true
}
.sheet(isPresented: $dialogDisplayed) {
Dialog(prompt: name == nil ? "Enter a name" : "Enter a new name", value: $name)
}
.onChange(of: name, perform: { value in
print("Name Changed : \(value)")
}
.padding()
}
.padding()
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
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!")
}
}
I found modals and alerts in SwiftUI to be lacking several features. For instance, there doesn't seem to be a way of presenting a modal with FormSheet style.
When I need to present a complex alert (such as one with textfields), I create a pure SwiftUI view with all the content of the alert, and then present it as a FormSheet using a UIHostController.
If you do not have a UIViewController around to call present(), you can always use the root view controller.
With this approach you get some nice features, such as the standard alert animation both going in, and out. You can also drag the alert down to dismiss it.
The alert view also moves up when the keyboard appears.
This works nicely on iPad. On iPhone, FormSheet is full screen so you may need to tweak the code to find a solution. I think this will give you a good starting point.
It is something like this:
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))
}
}
If you find yourself using this a lot, the button row may be encapsulated in a separate view for easy reuse.