Creating own character set causes crash - swift

I'm using this piece of code to limit user input regarding the keyboard.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let allowedCharacters = CharacterSet.init(charactersIn: ".0123456789")
let characterSet = NSCharacterSet(charactersIn: string)
return allowedCharacters.isSuperset(of: characterSet as CharacterSet)
}
but my program crashes when I enter any character.
Update 2:
So 99.9% of this solution works great, unfortunately the period/decimal point does not register. Unsure why this is happening?

This appears to be a bug in Swift, there are multiple issues on this matter on the Swift issue tracker: https://bugs.swift.org/browse/SR-3311, https://bugs.swift.org/browse/SR-3667
Until this has been fixed, you can workaround this problem by using the following extension:
extension CharacterSet {
func isSupersetOf(other: CharacterSet) -> Bool {
return CFCharacterSetIsSupersetOfSet(self as CFCharacterSet, (other as NSCharacterSet).copy() as! CFCharacterSet)
}
}
Keep in mind that you need to change your characterSet variable from type NSCharacterSet to CharacterSet to be able to use that extension in your example.

Related

Interesting confusion assigning an optional protocol method to a variable

Let's say I have a UITextField. I want to check if its delegate is set, and if so, check to see if a specific optional method is implemented. If it is implemented, then I need to call it, otherwise perform a fallback action. A common way to do this is:
let field = // some UITextField instance
if let delegate = field.delegate, delegate.responds(to: #selector(UITextFieldDelegate.textField(_:shouldChangeCharactersIn:replacementString:))) {
if delegate.textField?(field, shouldChangeCharactersIn: someRange, replacementString: someString) == true {
// do something here
}
} else {
// Fallback action
}
That is all working. But then I had an urge to try a different approach. Instead of using responds(to:) I want to assign the optional delegate method to a variable. I came up with the following which actually works:
Note: The following code requires a deployment target of iOS 15.0 to compile. If you set the target to iOS 16.0 then it doesn't compile. Not sure why.
if let delegate = field.delegate, let should = delegate.textField {
if should(field, field.selectedRange, title) {
// do something here
}
} else {
// Fallback action
}
While this works I'm really confused why it works.
The let should = delegate.textField part makes no reference to the method's parameters.
The UITextFieldDelegate protocol has 4 optional methods that start with textField. These are:
func textField(UITextField, shouldChangeCharactersIn: NSRange, replacementString: String) -> Bool
func textField(UITextField, editMenuForCharactersIn: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu?
func textField(UITextField, willDismissEditMenuWith: UIEditMenuInteractionAnimating)
func textField(UITextField, willPresentEditMenuWith: UIEditMenuInteractionAnimating)
So how does this work? How does the compiler know which one I meant? It actually seems to only work with the one that happens to be first.
I can't find any way to do this for the other 3 delegate methods. If I really want to call the func textField(UITextField, editMenuForCharactersIn: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? method I can't see how. The following code will not compile:
if let delegate = field.delegate, let editMenu = delegate.textField {
let suggested = [UIMenuElement]()
if let menu = editMenu(field, field.selectedRange, suggested) {
}
}
This gives the error:
Cannot convert value of type '[UIMenuElement]' to expected argument type 'String'
on the if let menu = should(field, field.selectedRange, suggested) { line. This clearly indicates it is assuming the func textField(UITextField, shouldChangeCharactersIn: NSRange, replacementString: String) -> Bool method.
Is there a syntax (that I'm missing) that allows me to assign the specific protocol method to a variable?
I'm going to stick with the tried and true use of responds(to:) for now but I'd love an explanation of what's going on with the attempts to assign the ambiguously named protocol method to a variable and if there is a way to specify the parameters to get the correct assignment.
My searching on SO didn't yield any relevant questions/answers.
My code is in an iOS project with a deployment target of iOS 15.0 and later using Xcode 14.0.1. The Swift compiler setting is set for Swift 5. It seems the code doesn't compile with a deployment target of iOS 16.0. Strange.
You can disambiguate either by strongly typing or spelling out the parameters. Neither of us knows why your code is compiling without disambiguating. It doesn't work for us.
if
let shouldChangeCharacters: (_, _, String) -> _ = field.delegate?.textField,
let editMenu: (_, _, Array) -> _ = field.delegate?.textField,
let willDismissEditMenu = field.delegate?.textField(_:willDismissEditMenuWith:),
let willPresentEditMenu = field.delegate?.textField(_:willPresentEditMenuWith:)
{ }
I'm not sure about the actual answer to your question, but I will say that your working code doesn't compile for me if I am targeting iOS 16. I get an error that says:
error: ambiguous use of 'textField'
if let delegate = textField.delegate, let should = delegate.textField {
UIKit.UITextFieldDelegate:13:19: note: found this candidate
optional func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
UIKit.UITextFieldDelegate:23:19: note: found this candidate
optional func textField(_ textField: UITextField, editMenuForCharactersIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu?
But instead of holding onto the function with your should variable, why not unwrap the result as a part of your if statement?
Something like this:
if let delegate = textField.delegate,
let should = delegate.textField?(field, shouldChangeCharactersIn: someRange, replacementString: someString) {
if should {
// perform some action
}
} else {
// perform fallback action
}
The resulting logic should be the same.

how to remove character when backspace is pressed swift

i want to remove one last character when user presses the backspace
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.isEmpty {
print("backspace pressed")
if let itemToRemove = textField.text?.dropLast(){
let text = textField.text?.replacingOccurrences(of: itemToRemove, with: "")
textField.text = text
return true
}
}
return true
}
this function clears all the elements present in the textfield
You're using this method wrong. The delegate method is not for you to implement the change to the text, it's for you to approve the change (hence returning a bool).
From the documentation (which is always a good first point of call if something isn't working how you expect):
Return Value
true if the specified text range should be replaced;
otherwise, false to keep the old text.
Discussion
The text field calls this method whenever user actions cause its text to change. Use this
method to validate text as it is typed by the user. For example, you
could use this method to prevent the user from entering anything but
numerical values.
EDIT: (as pointed out by Duncan C in the comments, and as should have been in the original answer)
A good starting point is just to return true from this method, as then all the user input will be reflected in the text field. If you need to be more specific about what edits you allow you can introduce that logic later.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Backspace handled
guard !string.isEmpty else {
return true
}
return true
}

checking validity of decimal input in UITextfield

One of the UITextfield input should accept decimals. But I want to restrict the user to type only one ".", otherwise the floating point conversion gets messed up if it has more than one "." in the number. How to ensure user does not key in more than one "." as a part of decimal input?
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if ( textField == areaTextField) {
guard CharacterSet(charactersIn: "0123456789.").isSuperset(of: CharacterSet(charactersIn: string)) else {
return false
}
}
return true
}
Your approach is wrong to start with; you are way overthinking this. The computer already knows whether a string is a valid number, so just ask it. Don't examine any characters. Don't examine any keys. Simply perform the replacement described by the parameters and ask the computer if the result would be a number:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let rep = (textField.text! as NSString).replacingCharacters(in: range, with: string)
return rep.count == 0 || Double(rep) != nil
}

How to properly solve the EXC_BAD_ACCESS (code=EXC_i386_GPFLT) error in my function

Ok, so total Swift noob here. Going through some beginner tutorials.
In order to achieve some goal,
I need a UITextField to accept only numbers
I have managed to get the UITextField to accept only letters via this code:
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet var TextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.TextField.delegate = self
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let allowedCharacters = CharacterSet.letters
let characterSet = CharacterSet(charactersIn: string)
return allowedCharacters.isSuperset(of: characterSet)
}
}
However as soon as I turn allowedCharacters into something like:
let allowedCharacters = CharacterSet(charactersIn: "0123456789")
The Simulator still launches successfully (and I get a Build Succeeded message)
However, as soon as I start typing it crashes. Xcode then highlights my return statement and shows the error message:
Thread 1: EXC_BAD_ACCESS (code=EXC_i386_GPFLT)
When I google for it I get the sense that it might have something to do with the fact that I might need to an if-statement somewhere in my function to catch any unexpected values that I guess are being returned as soon as I start typing in the textfield, but I can not see the logic yet as to where and how.
Any thoughts to enlighten me would be great!
PS: I know I could also just configure my textfield to only show a numpad, but I'd still like to know why the current solution is causing problems for I might need other, more advanced, restrictions sometime in the future.
EDIT
Another solution that works (which I discovered after posting the question) is to change my declaration of allowedCharacters as such:
let allowedCharacters = CharacterSet.decimalDigits
However, the accepted answer to this question allows for more finetuning when necessary.
If you want to handle it that way, instead of making a character set out of your string and checking if it's a subset you should use string's method rangeOfCharacter(from: CharacterSet) -> Range<String.Index>?. This method returns a position of a character from given set or nil if there was no character from the set present.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
print(string)
let allowedCharacters = CharacterSet(charactersIn: "0123456789")
return string.rangeOfCharacter(from: allowedCharacters) != nil
}
However it is not a good approach to specify allowed characters as here you'll prevent user from removing characters with backspace. You should rather specify disallowed characters then this function will change to:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
print(string)
let disallowedCharacters = CharacterSet(charactersIn: "0123456789")
return string.rangeOfCharacter(from: disallowedCharacters) == nil
}

How to prevent username textfield for not using whitespace or special characters swift 3.0 [duplicate]

I am creating a trivia application that asks for a username on start up. I'd like to make it impossible to use characters such as #$#!^& etc (also including "space"). I took a look at this post here but it is written entirely in Objective-C. Thanks in advance.
Swift 4 iOS 11.2.x based on using an extension, tests to see if a string is a valid hex number in this example.
extension String {
var containsValidCharacter: Bool {
guard self != "" else { return true }
let hexSet = CharacterSet(charactersIn: "1234567890ABCDEFabcdef")
let newSet = CharacterSet(charactersIn: self)
return hexSet.isSuperset(of: newSet)
}
}
You use it like with the UITextFieldDelegate.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return (string.containsValidCharacter)
}
Although I read in an earlier post that CharacterSets do not support characters that are composed of more than one Unicode.Scalar; so use with caution I guess.
Since you're explicitly asking for Swift, I've translated the top asnwer in the linked question.
let notAllowedCharacters = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.";
func textField(
textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String)
-> Bool
{
let set = NSCharacterSet(charactersInString: notAllowedCharacters);
let inverted = set.invertedSet;
let filtered = string
.componentsSeparatedByCharactersInSet(inverted)
.joinWithSeparator("");
return filtered != string;
}
internal func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool{
if let text = string{
if text == "#" || text == "$" || text == "!"{ \\and so on
return false
}
}
return true
}
Swift 2.3
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let characters = ["#", "$", "!", "&","#"]
for character in characters{
if string == character{
print("This characters are not allowed")
return false
}
}
}
So this is probably the most robust way to restrict Spaces. Using this user won't be able to Paste/Type Whitespaces
This is how you can Implement using Swift 3.
Add below mentioned extension snippet to a Swift file;
extension String {
var containsWhitespace: Bool {
for scalar in unicodeScalars {
switch scalar.value {
case 0x20:
return true
default:
continue
}
}
return false
}
}
In your ViewController Swift file drag out your Editing Changed Instance and a Referencing Outlet of UITextField from Storyboard, the one mentioned in picture below:
Use the dragged Instances as mentioned below:
Referencing Outlet as:
#IBOutlet weak var textField: UITextField!
and Editing Changed as:
#IBAction func textChanged(_ sender: UITextField) {
if (textField.text!.containsWhitespace) == true {
print("Restrict/Delete Whitespace")
emailField.deleteBackward()
} else {
print("If Its not Whitespace, Its allowed.")
}
}
This will detect and remove whitespace as soon as user tries to type/paste it.
Swift 4 iOS 11.2.x based on using an extension, tests to see if a string is a valid hex number in this example.
extension String {
var containsValidCharacter: Bool {
guard self != "" else { return true }
let hexSet = CharacterSet(charactersIn: "1234567890ABCDEFabcdef")
let newSet = CharacterSet(charactersIn: self)
return hexSet.isSuperset(of: newSet)
}
}
You use it like with the UITextFieldDelegate.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return (string.containsValidCharacter)
}
Swift : 3 and a different approach:
Add a target function for the text field change in your viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(ViewController.textFieldDidChange(textField:)), for: UIControlEvents.editingChanged)
}
in the target function, simply detect the entered char and replace it with blank. I have tested it and it prevents the user from entering any non desirable characters in the text field.
func textFieldDidChange(textField: UITextField) {
if let textInField = textField.text{
if let lastChar = textInField.characters.last{
//here include more characters which you don't want user to put in the text field
if(lastChar == "*")
{
textField.text = textInField.substring(to: textInField.index(before: textInField.endIndex))
}
}
}
}
Adding on to what #Evdzhan Mustafa said. You want to add a return statement in case the string is empty. Without it you won't be able to delete your text. Modified Code Below:
Swift 3 Version
let notAllowedCharacters = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.";
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.isEmpty{
return true
}
print("String: \(string)")
let set = NSCharacterSet(charactersIn: notAllowedCharacters);
let inverted = set.inverted;
let filtered = string.components(separatedBy: inverted).joined(separator: "")
print("String Filtered: \(filtered)")
return filtered != string;
}