I need to use UIAlertContoller, as SwiftUI's Alert does not support TextField.
I must not use Custom created AlertView, due to various reason(Accessibility, DynamicType, Dark Mode support etc).
Basic Idea is, SwiftUI's alert must hold TextField & entered text must be reflect back for usage.
I created a SwiftUI view by Conforming to UIViewControllerRepresentable following is working code.
struct AlertControl: UIViewControllerRepresentable {
typealias UIViewControllerType = UIAlertController
#Binding var textString: String
#Binding var show: Bool
var title: String
var message: String
func makeUIViewController(context: UIViewControllerRepresentableContext<AlertControl>) -> UIAlertController {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "Enter some text"
}
let cancelAction = UIAlertAction(title: "cancel", style: .destructive) { (action) in
self.show = false
}
let submitAction = UIAlertAction(title: "Submit", style: .default) { (action) in
self.show = false
}
alert.addAction(cancelAction)
alert.addAction(submitAction)
return alert
}
func updateUIViewController(_ uiViewController: UIAlertController, context: UIViewControllerRepresentableContext<AlertControl>) {
}
func makeCoordinator() -> AlertControl.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
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 {
self.control.textString = text
}
return true
}
}
}
// SwiftUI View in some content view
AlertControl(textString: self.$text,
show: self.$showAlert,
title: "Title goes here",
message: "Message goes here")
Problem:
There is No activity in Alert Action when it is tapped. I put breakpoints to check, but it never hit there.
Even UITextFieldDelegate's function never hit.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
Edit: cancelAction or submitAction does not triggers on tap of these fields.
Here is full demo module for solution that works. Tested with Xcode 11.4 / iOS 13.4
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)
}
}
}
Related
I want to use an alert with textField in my swiftUI app so I made following wrapper for UIAlertController. I want to disable my save button when there is no text in textField and enable it the moment user enters some text, but for some reason it is not happening, the button is stuck to the initial state.
code
struct UIAlertControllerWrapper: UIViewControllerRepresentable {
#Binding var text: String
#Binding var showAlert: Bool
var title: String
var message: String
var placeholder: String
var action: () -> Void
func makeUIViewController(context: Context) -> some UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
if showAlert {
let alert = createAlert(context)
context.coordinator.alert = alert
alert.actions[1].isEnabled = false
DispatchQueue.main.async {
uiViewController.present(alert, animated: true) {
showAlert = false
}
}
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: UIAlertControllerWrapper
var alert: UIAlertController?
init(_ parent: UIAlertControllerWrapper) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text as? NSString {
parent.text = text.replacingCharacters(in: range, with: string)
} else {
parent.text = ""
}
guard let alert = alert else {
return true
}
if parent.text.trimmingCharacters(in: .whitespaces).isEmpty {
alert.actions[1].isEnabled = false
} else {
alert.actions[1].isEnabled = true
}
return true
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func createAlert(_ context: Context) -> UIAlertController {
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: .alert
)
alert.addTextField { textField in
textField.placeholder = placeholder
textField.text = text
textField.delegate = context.coordinator
}
let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in
alert.dismiss(animated: true) {
showAlert = false
}
text = ""
}
let save = UIAlertAction(title: "Save", style: .default) { _ in
action()
alert.dismiss(animated: true) {
showAlert = false
}
text = ""
}
alert.addAction(cancel)
alert.addAction(save)
return alert
}
}
I'm working on a project titled "apple pie" and I'm trying to figure out if there is a way for a user to enter their name on a pop-up alert, I coded, and for that name, that they typed, to appear or re-write a blue background label on the game.
This is all the code I have so far:
import UIKit
class ViewController: UIViewController {
#IBOutlet var treeImageView: UIImageView!
#IBOutlet var correctWordLabel: UILabel!
#IBOutlet var scoreLabel: UILabel!
#IBOutlet var letterButtons: [UIButton]!
//Player name output to blue label
#IBOutlet weak var textfield: UILabel!
//Click to enter player name
#IBAction func enterButton(_ sender: Any) {
// textfield.text = textbox.text
showNamePopup()
}
var listOfWords = ["mistery", "stars", "banana", "igloo", "bug", "programming", "car", "motorcycle", "weasel", "cat", "bazzinga"]
let incorrectMovesAllowed = 7
var totalWins = 0 {
didSet {
newRound()
}
}
var totalLosses = 0 {
didSet {
newRound()
}
}
var currentGame: Game!
override func viewDidLoad() {
super.viewDidLoad()
newRound()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func newRound() {
if !listOfWords.isEmpty {
let newWord = listOfWords.removeFirst()
currentGame = Game(word: newWord, incorrectMovesRemaining: incorrectMovesAllowed, guessedLetters: [])
enableLetterButtons(true)
updateUI()
} else {
enableLetterButtons(false)
}
}
func enableLetterButtons(_ enable: Bool) {
for button in letterButtons {
button.isEnabled = enable
}
}
func updateUI() {
var letters = [String]()
for letter in currentGame.formattedWord {
letters.append(String(letter))
}
let wordWithSpacing = letters.joined(separator: " ")
correctWordLabel.text = wordWithSpacing
scoreLabel.text = "Wins: \(totalWins), Losses: \(totalLosses)"
treeImageView.image = UIImage(named: "Tree \(currentGame.incorrectMovesRemaining)")
}
#IBAction func letterButtonPressed(_ sender: UIButton) {
sender.isEnabled = false
let letterString = sender.title(for: .normal)!
let letter = Character(letterString.lowercased())
currentGame.playerGuessed(letter: letter)
updateGameState()
}
func updateGameState() {
if currentGame.incorrectMovesRemaining == 0 {
totalLosses += 1
} else if currentGame.word == currentGame.formattedWord {
totalWins += 1
} else {
updateUI()
}
}
/// Pop-Up Coding
private func showNamePopup(){
// Make a confirmation alert dialog.
let alert = UIAlertController(
title: "Enter Name",
message: "Enter player name if you wish to save progess",
preferredStyle: .alert
)
// Add a text field to get user name.
alert.addTextField { textField in _ = textField.text}
// Add cancel action.
alert.addAction(UIAlertAction(title: "Cancel",style: .destructive, handler: { _ in} ))
// Handle state if user cancels the pop-up.
// Add ok action.
alert.addAction(UIAlertAction(title: "Ok",style: .default))
// Show pop-up. Add a completion when pop-up is destroyed.
self.present(alert, animated: true, completion: nil );
}
}
A pop-up of user input:
Apple pie game:
You need to add a completion handler to the 'OK' action in your alert.
private func showNamePopup(){
// Make a confirmation alert dialog.
let alert = UIAlertController(
title: "Enter Name",
message: "Enter player name if you wish to save progess",
preferredStyle: .alert
)
// Add a text field to get user name.
alert.addTextField { textField in _ = textField.text}
// Add cancel action.
alert.addAction(UIAlertAction(title: "Cancel",style: .destructive, handler: { _ in} ))
// Handle state if user cancels the pop-up.
// Add ok action.
let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in
if let name = alert.textFields?.first?.text {
self?.nameLabel.text = name // Show the name in the text label
}
}
alert.addAction(okAction)
// Show pop-up. Add a completion when pop-up is destroyed.
self.present(alert, animated: true, completion: nil );
}
Whenever I click on a UITextField, a list of contacts appear and when a contact is clicked, the phone number should appear in the textfield that was clicked. I currently have 3 textfields and each time I select a contact, it updates only the first textfield even if for example I have selected the 2nd textfield. How do I go about fixing it so that the phone number appears in the corresponding textfield that was selected?
I'm using Xcode 10 and I think that the issue is arising from the func setNumberFromContact
#IBOutlet weak var phonenumber: UITextField!
#IBOutlet weak var phonenumber1: UITextField!
#IBOutlet weak var phonenumber2: UITextField!
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
let phoneNumberCount = contact.phoneNumbers.count
guard phoneNumberCount > 0 else {
dismiss(animated: true)
//show pop up: "Selected contact does not have a number"
return
}
if phoneNumberCount == 1 {
setNumberFromContact(contactNumber: contact.phoneNumbers[0].value.stringValue)
}else{
let alertController = UIAlertController(title: "Select one of the numbers", message: nil, preferredStyle: .alert)
for i in 0...phoneNumberCount-1 {
let phoneAction = UIAlertAction(title: contact.phoneNumbers[i].value.stringValue, style: .default, handler: {
alert -> Void in
self.setNumberFromContact(contactNumber: contact.phoneNumbers[i].value.stringValue)
})
alertController.addAction(phoneAction)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .destructive, handler: {
alert -> Void in
})
alertController.addAction(cancelAction)
dismiss(animated: true)
self.present(alertController, animated: true, completion: nil)
}
}
func setNumberFromContact(contactNumber: String) {
var contactNumber = contactNumber.replacingOccurrences(of: "-", with: "")
contactNumber = contactNumber.replacingOccurrences(of: "(", with: "")
contactNumber = contactNumber.replacingOccurrences(of: ")", with: "")
guard contactNumber.count >= 10 else {
dismiss(animated: true) {
self.presentAlert(alertTitle: "", alertMessage: "A maximum of 10 contacts allowed per session", lastAction: nil)
}
return
}
phonenumber.text = String(contactNumber.suffix(10))
}
func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
}
}
extension SelfTestTimer: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField.hasText{
//dont do anything
}else{
contactPicker.delegate = self
self.present(contactPicker, animated: true, completion: nil)
}
return
}
The reason your solution is updating only one text field is because you're updating the text of only that text field. In this line phonenumber.text = String(contactNumber.suffix(10)) you change only phonenumber's text. A good solution would be as follows:
Create a temp UITextField to store selected text field reference
#IBOutlet weak var phonenumber: UITextField!
#IBOutlet weak var phonenumber1: UITextField!
#IBOutlet weak var phonenumber2: UITextField!
var currentTextField: UITextField?
And use that text field in text field delegate methods
extension SelfTestTimer: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
currentTextField = nil
textField.resignFirstResponder()
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
if textField.hasText{
//dont do anything
}else{
currentTextField = textField
contactPicker.delegate = self
self.present(contactPicker, animated: true, completion: nil)
}
return
}
}
Assign selected contact number in that text field
func setNumberFromContact(contactNumber: String) {
var contactNumber = contactNumber.replacingOccurrences(of: "-", with: "")
contactNumber = contactNumber.replacingOccurrences(of: "(", with: "")
contactNumber = contactNumber.replacingOccurrences(of: ")", with: "")
guard contactNumber.count >= 10 else {
dismiss(animated: true) {
self.presentAlert(alertTitle: "", alertMessage: "A maximum of 10 contacts allowed per session", lastAction: nil)
}
return
}
currentTextField?.text = String(contactNumber.suffix(10))
}
How can I check if the value from an UITextField is an interval of numbers between 1 - 100 and IF the number IS in that interval to send the value to another UIViewController? If the value in not in that interval then to show an alert.
My other controller have a var receivedValue = "" which I will use it to populate a UILabel.
Here is my code:
import UIKit
class ChildViewController: UIViewController {
#IBOutlet weak var insertNumberTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
insertNumberTextField.delegate = self
}
#IBAction func checkNumberIntervalButton(_ sender: UIButton) {
if insertNumberTextField.text == "\(1..<100)"{
print("Number is in interval 1 - 100.")
navigationController?.popViewController(animated: true)
dismiss(animated: true, completion: nil)
} else {
let alert = UIAlertController(title: "Try again", message: "Sorry but this numer is not in the inverval 1 - 100. Try again.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
insertNumberTextField.text = ""
print("Number is not in the inverval 1 - 100.")
}
}
}
extension ChildViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let allowedCharacters = CharacterSet.decimalDigits
let characterSet = CharacterSet(charactersIn: string)
return allowedCharacters.isSuperset(of: characterSet)
}
}
Convert the text field's text into an Int and compare it that way.
if let text = insertNumberTextField.text, let value = Int(text), (0 ..< 100).contains(value) {
// number is between 1 and 100
} else {
// show alert
}
I have found this sample code about how to add the textfield for the UIAlertController but I am unable to restrict the the users to enter only 10 characters as a limit, I have a function to remove the last character when it is go over when editing changed for a regular textfield but unable to check the textfield on the UIAlertController.
//Function to Remove the last character
func trimStr(existStr:String)->String{
var countExistTextChar = countElements(existStr)
if countExistTextChar > 10 {
var newStr = ""
newStr = existStr.substringToIndex(advance(existStr.startIndex,(countExistTextChar-1)))
return newStr
}else{
return existStr
}
}
var inputTextField: UITextField?
//Create the AlertController
let actionSheetController: UIAlertController = UIAlertController(title: "Alert", message: "Swiftly Now! Choose an option!", preferredStyle: .Alert)
//Create and add the Cancel action
let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .Cancel) { action -> Void in
//Do some stuff
}
actionSheetController.addAction(cancelAction)
//Create and an option action
let nextAction: UIAlertAction = UIAlertAction(title: "Next", style: .Default) { action -> Void in
}
actionSheetController.addAction(nextAction)
//Add a text field
**actionSheetController.addTextFieldWithConfigurationHandler { textField -> Void in
//TextField configuration
textField.textColor = UIColor.blueColor()
inputTextField = textField**
}
//Present the AlertController
self.presentViewController(actionSheetController, animated: true, completion: nil)
Thank you.
You can set the delegate of the alert controller's text field as
for example showed here https://stackoverflow.com/a/24852979/1187415:
actionSheetController.addTextFieldWithConfigurationHandler { [weak self] textField -> Void in
//TextField configuration
textField.textColor = UIColor.blueColor()
textField.delegate = self
}
Note that you have to add the UITextFieldDelegate protocol to your
view controller definition to make that compile:
class ViewController: UIViewController, UITextFieldDelegate {
// ...
Then you implement the shouldChangeCharactersInRange delegate method,
which is called whenever the text is about to be changed in response
to user interaction. It can return true or false to allow the
change or to deny it.
There are various examples how to limit the text field length in
this delegate method, here is one possible implementation:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string) as NSString
return newString.length <= 10
}
This will cause input to be ignored if it would cause the text field
length to be greater than 10. Alternatively you could always truncate
the input to 10 characters:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
textField.text = trimStr(newString)
return false
}
A disadvantage of the second method is that the cursor always jumps
to the end of the text field, even if text is inserted somewhere
in between.