Swiftui Present Alert with Input Field

SwiftUI using .alert(isPresented) with a TextField for input?


extension UIAlertController {
convenience init(alert: AlertConfig) {
self.init(title: alert.title, message: nil, preferredStyle: .alert)
addTextField { $0.placeholder = alert.placeholder }
addAction(UIAlertAction(title: alert.cancel, style: .cancel) { _ in
alert.action(nil)
})
let textField = self.textFields?.first
addAction(UIAlertAction(title: alert.accept, style: .default) { _ in
alert.action(textField?.text)
})
}
}



struct AlertHelper<Content: View>: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let alert: AlertConfig
let content: Content

func makeUIViewController(context: UIViewControllerRepresentableContext<AlertHelper>) -> 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<AlertHelper>) {
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)
}
}
}

public struct AlertConfig {
public var title: String
public var placeholder: String = ""
public var accept: String = "OK"
public var cancel: String = "Cancel"
public var action: (String?) -> ()
}

extension View {
public func alert(isPresented: Binding<Bool>, _ alert: AlertConfig) -> some View {
AlertHelper(isPresented: isPresented, alert: alert, content: self)
}
}

struct ContentView: View {
@State var isAlert = false
var body: some View {
VStack {
Button("Show Alert") {
self.isAlert = true
}
}
.alert(isPresented: $isAlert, AlertConfig(title: "Title", action: {
print("Text \($0 ?? "Cancelled")")
}))
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to add a TextField to Alert in SwiftUI?

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

Is it possible to save the text entered into an alert's textfield in SwiftUI and display it in the app like this - Text( alert text input here )?

You will want to grab the value of the text field from inside the Done button's handler. So all you need to do is move the bracket down:

alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
let textField = alert.textFields![0] as UITextField
alertInput = textField.text ?? "Name"
})

The code inside this closure is called when the user taps this UIAlertAction you have set up.

Note that if the field is left empty, the value of textField.text will be "" and not nil, so in order to use the default value you provide, you may need some additional logic here to check for a blank string as well.

SwiftUI: UIAlertController's textField does not responding in UIAlertAction

Here is full demo module for solution that works. Tested with Xcode 11.4 / iOS 13.4

demo

See also important comments inline

struct AlertControl: UIViewControllerRepresentable {

@Binding var textString: String
@Binding var show: Bool

var title: String
var message: String

func makeUIViewController(context: UIViewControllerRepresentableContext<AlertControl>) -> UIViewController {
return UIViewController() // holder controller - required to present alert
}

func updateUIViewController(_ viewController: UIViewController, context: UIViewControllerRepresentableContext<AlertControl>) {
guard context.coordinator.alert == nil else { return }
if self.show {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
context.coordinator.alert = alert

alert.addTextField { textField in
textField.placeholder = "Enter some text"
textField.text = self.textString // << initial value if any
textField.delegate = context.coordinator // << use coordinator as delegate
}
alert.addAction(UIAlertAction(title: "cancel", style: .destructive) { _ in
// your action here
})
alert.addAction(UIAlertAction(title: "Submit", style: .default) { _ in
// your action here
})

DispatchQueue.main.async { // must be async !!
viewController.present(alert, animated: true, completion: {
self.show = false // hide holder after alert dismiss
context.coordinator.alert = nil
})
}
}
}

func makeCoordinator() -> AlertControl.Coordinator {
Coordinator(self)
}

class Coordinator: NSObject, UITextFieldDelegate {
var alert: UIAlertController?
var control: AlertControl
init(_ control: AlertControl) {
self.control = control
}

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text as NSString? {
self.control.textString = text.replacingCharacters(in: range, with: string)
} else {
self.control.textString = ""
}
return true
}
}
}

// Demo view for Alert Controll
struct DemoAlertControl: View {
@State private var text = ""
@State private var showAlert = false

var body: some View {
VStack {
Button("Alert") { self.showAlert = true }
.background(AlertControl(textString: self.$text, show: self.$showAlert,
title: "Title goes here", message: "Message goes here"))
Text(self.text)
}
}
}

config 2 alerts messages in button swiftUI

For multiple alerts in a single view, you can use an enum.

First, you need to create an enum like this and define all the alert message

enum AlertType: Identifiable {
var id: UUID {
return UUID()
}

case success
case error

var title: String {
switch self {
case .success:
return "Success !"
case .error:
return "Error !"
}
}

var message: String {
switch self {
case .success:
return "Insert with success !"
case .error:
return "This category already exist !!"
}
}
}

now create one state var in the view.

struct NewCategoryView: View {
@State private var alertType: AlertType?

// Other code
}

and add the alert at the end

//end Vstak
.navigationTitle("New Category")
.onAppear(perform: { updateCategoryList()} )
.alert(item: self.$alertType, content: { (type) -> Alert in
Alert(title: Text(type.title), message: Text(type.message),
dismissButton: .default(Text("OK")))
})

now show the alert by assigning the value. Like this

if condition_true {
alertType = AlertType.success //<-- Here
} else {
alertType = AlertType.error //<-- Here
}

Present alert on previous screen SwiftUI

you could try this approach using a simple binding:

import SwiftUI
import PopupView

@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView : View {
var body: some View {
NavigationView {
HelpView()
}
}
}
struct HelpView: View {
@State var showPopup = false // <-- here

var body: some View {
ZStack {
Color.pink.ignoresSafeArea()
List {
NavigationLink(destination: FeedbackView(showPopup: $showPopup)) { // <-- here
Text("Report a problem").fontWeight(.semibold)
}
HStack {
Link(destination: URL(string: "https://sauced.app")!) {
Text("Help Center").fontWeight(.semibold)
}
Spacer()
Image(systemName: "chevron.right").foregroundColor(Color.black)
}
}
}
.popup(isPresented: $showPopup, autohideIn: 3) {
Text("THANK YOU")
.frame(width: 200, height: 60)
.background(Color(red: 0.85, green: 0.8, blue: 0.95))
.cornerRadius(30.0)
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle("Help")
}
}
struct FeedbackView : View {
@Environment(\.dismiss) var dismiss
@Binding var showPopup: Bool // <-- here

var body: some View {
Button(action: {
showPopup = true // <-- here
dismiss()
}, label: {
Text("Send").fontWeight(.semibold)
})
}
}

Get input value from TextField in iOS alert in Swift

Updated for Swift 3 and above:

//1. Create the alert controller.
let alert = UIAlertController(title: "Some Title", message: "Enter a text", preferredStyle: .alert)

//2. Add the text field. You can configure it however you need.
alert.addTextField { (textField) in
textField.text = "Some default text"
}

// 3. Grab the value from the text field, and print it when the user clicks OK.
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak alert] (_) in
let textField = alert.textFields![0] // Force unwrapping because we know it exists.
print("Text field: \(textField.text)")
}))

// 4. Present the alert.
self.present(alert, animated: true, completion: nil)

Swift 2.x

Assuming you want an action alert on iOS:

//1. Create the alert controller.            
var alert = UIAlertController(title: "Some Title", message: "Enter a text", preferredStyle: .Alert)

//2. Add the text field. You can configure it however you need.
alert.addTextFieldWithConfigurationHandler({ (textField) -> Void in
textField.text = "Some default text."
})

//3. Grab the value from the text field, and print it when the user clicks OK.
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { [weak alert] (action) -> Void in
let textField = alert.textFields![0] as UITextField
println("Text field: \(textField.text)")
}))

// 4. Present the alert.
self.presentViewController(alert, animated: true, completion: nil)


Related Topics



Leave a reply



Submit