I have a textfield and I do not want users to be able to set the cursor position by touching and holding at the textfield. Is there a way to always have the cursor to be at the very end of the textfield?
First of all, its not good to change this type of default behavior of textField.
As per your comment if you want to add credit card and you are facing some input complexity then I found code from my old project and might be it will work for you.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let replacementStringIsLegal = string.rangeOfCharacter(from: NSCharacterSet(charactersIn: "0123456789").inverted) == nil
if !replacementStringIsLegal {
return false
}
let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
let components = newString.components(separatedBy: NSCharacterSet(charactersIn: "0123456789").inverted)
let decimalString = components.joined(separator: "") as NSString
let length = decimalString.length
let hasLeadingOne = length > 0 && decimalString.character(at: 0) == (1 as unichar)
if length == 0 || (length > 16 && !hasLeadingOne) || length > 19
{
let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int
return (newLength > 16) ? false : true
}
var index = 0 as Int
let formattedString = NSMutableString()
if hasLeadingOne
{
formattedString.append("1 ")
index += 1
}
if length - index > 4
{
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.appendFormat("%# ", prefix)
index += 4
}
if length - index > 4
{
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.appendFormat("%# ", prefix)
index += 4
}
if length - index > 4
{
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.appendFormat("%# ", prefix)
index += 4
}
let remainder = decimalString.substring(from: index)
formattedString.append(remainder)
textField.text = formattedString as String
return false
}
Related
I'm trying to autoformat my textfield in the format XXX-XXX-XXXX. The rules are that it should be in the format as mentioned and the first number should be greater than zero and should be of max 10 digits, the regex for this is already added in my function. Below are the methods I'm using
#IBAction func validateAction(_ sender: Any) {
guard let phoneNumber = phoneNumber.text else {return }
if validatePhoneNumber(phoneNumber: phoneNumber) {
errorMessage.text = "Validation successful"
} else {
errorMessage.text = "Validation failed"
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let currentText = textField.text as NSString? else {return true}
let textString = currentText.replacingCharacters(in: range, with: string)
if textField == phoneNumber {
return textField.updatePhoneNumber(string, textString)
}else{
return true
}
}
func validatePhoneNumber(phoneNumber: String) -> Bool {
let phoneRegex: String = "^[2-9]\\d{2}-\\d{3}-\\d{4}$"
return NSPredicate(format: "SELF MATCHES %#", phoneRegex).evaluate(with: phoneNumber)
}
extension UITextField {
func updatePhoneNumber(_ replacementString: String?, _ textString: String?) -> Bool {
guard let textCount = textString?.count else {return true}
guard let currentString = self.text else {return true}
if replacementString == "" {
return true
} else if textCount == 4 {
self.text = currentString + "-"
} else if textCount == 8 {
self.text = currentString + "-"
} else if textCount > 12 || replacementString == " " {
return false
}
return true
}
}
This works to some extent, now the issue is, user can manually intervene and disrupt the format for eg: if I entered, 234-567-8990, user can place the cursor just before 5 and backspace and type in at the end or between like 567-89900000 or 234567-8990. By validating the regular expression it will give an error but I want to re-adjust the format as user types in. For eg: in the earlier scenario if the user is on cursor before 5 and backspaces it should not remove the dash (-) but just removes 4 and re-adjust format like 235-678-990. Is there any simple way to do this? Any help is appreciated
I use this extension for String. It's small and real helpful.
extension String {
func applyPatternOnNumbers(pattern: String, replacmentCharacter: Character) -> String {
var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression)
for index in 0 ..< pattern.count {
guard index < pureNumber.count else { return pureNumber }
let stringIndex = String.Index(encodedOffset: index)
let patternCharacter = pattern[stringIndex]
guard patternCharacter != replacmentCharacter else { continue }
pureNumber.insert(patternCharacter, at: stringIndex)
}
return pureNumber
}
just set a needed mask
text.applyPatternOnNumbers(pattern: "+# (###) ###-##-##", replacmentCharacter: "#")
and that's all
#SonuP very good question. I believe you want to format the phone and also keep the cursor in correct position. If so, then this task is slightly more complex than just formatting. You need to reformat the code and update the cursor position.
Note that my solution follows the specific formatting and if it does not match yours, then tweak the code slightly:
Swift 5
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var text = textField.text ?? ""
text.replaceSubrange(range.toRange(string: text), with: string)
if let phone = (textField.text ?? "").replacePhoneSubrange(range, with: string) {
// update text in the field
textField.text = text
// update cursor position
if text.count == range.location + string.count || text.hasSuffix(")") && text.count == range.location + string.count + 1 { // end
if phone.hasSuffix(")") {
textField.setCursor(phone.count - 1)
}
else {
textField.setCursor(phone.count)
}
}
else {
textField.setCursor(min(range.location + string.count, phone.count-1))
}
}
return false
}
Also you will need the following extensions:
// MARK: - Helpful methods
extension NSRange {
/// Convert to Range for given string
///
/// - Parameter string: the string
/// - Returns: range
func toRange(string: String) -> Range<String.Index> {
let range = string.index(string.startIndex, offsetBy: self.lowerBound)..<string.index(string.startIndex, offsetBy: self.upperBound)
return range
}
static func fromRange(_ range: Range<String.Index>, inString string: String) -> NSRange {
let s = string.distance(from: string.startIndex, to: range.lowerBound)
let e = string.distance(from: string.startIndex, to: range.upperBound)
return NSMakeRange(s, e-s)
}
}
// MARK: - Helpful methods
extension String {
/// Raplace string in phone
public func replacePhoneSubrange(_ range: NSRange, with string: String) -> String? {
if let phone = self.phone { // +11-111-111-1111 (111)
var numberString = phone.phoneNumber // 111111111111111
let newRange = self.toPhoneRange(range: range)
numberString.replaceSubrange(newRange.toRange(string: phone), with: string)
return numberString.phone
}
return nil
}
/// Phone number string
public var phoneNumber: String {
let components = self.components(
separatedBy: CharacterSet.decimalDigits.inverted)
var decimalString = NSString(string: components.joined(separator: ""))
while decimalString.hasPrefix("0") {
decimalString = decimalString.substring(from: 1) as NSString
}
return String(decimalString)
}
/// Get phone range
public func toPhoneRange(range: NSRange) -> NSRange {
let start = range.location
let end = start + range.length
let s2 = self.convertPhoneLocation(location: start)
let e2 = self.convertPhoneLocation(location: end)
return NSRange(location: s2, length: e2-s2)
}
/// Get cursor location for phone
public func convertPhoneLocation(location: Int) -> Int {
let substring = self[self.startIndex..<self.index(self.startIndex, offsetBy: location)]
return String(substring).phoneNumber.count
}
}
// MARK: - Helpful methods
extension UITextField {
/// Set cursor
///
/// - Parameter position: the position to set
func setCursor(_ position: Int) {
if let startPosition = self.position(from: self.beginningOfDocument, offset: position) {
let endPosition = startPosition
self.selectedTextRange = self.textRange(from: startPosition, to: endPosition)
}
}
}
// MARK: - Helpful methods
extension String {
/// phone formatting
public var phone: String? {
let components = self.components(
separatedBy: CharacterSet.decimalDigits.inverted)
var decimalString = NSString(string: components.joined(separator: ""))
while decimalString.hasPrefix("0") {
decimalString = decimalString.substring(from: 1) as NSString
}
let length = decimalString.length
let hasLeadingOne = length > 0 && length == 11
let hasLeadingTwo = length > 11
if length > 15 {
return nil
}
var index = 0 as Int
let formattedString = NSMutableString()
if hasLeadingOne || hasLeadingTwo {
let len = hasLeadingTwo ? 2 : 1
let areaCode = decimalString.substring(with: NSMakeRange(index, len))
formattedString.appendFormat("+%#-", areaCode)
index += len
}
if (length - index) > 3 {
let areaCode = decimalString.substring(with: NSMakeRange(index, 3))
formattedString.appendFormat("%#-", areaCode)
index += 3
}
if length - index == 4 && length == 7 { // xxx-xxxx
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.append(prefix)
index += 4
}
else if length - index > 3 {// xxx-xxx-x...
let prefix = decimalString.substring(with: NSMakeRange(index, 3))
formattedString.appendFormat("%#-", prefix)
index += 3
}
if length - index == 4 { // xxx-xxx-xxxx
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.append(prefix)
index += 4
}
// format phone extenstion
if length - index > 4 {
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.appendFormat("%# ", prefix)
index += 4
}
let remainder = decimalString.substring(from: index)
if length > 12 {
formattedString.append("(\(remainder))")
}
else {
formattedString.append(remainder)
}
return (formattedString as String).trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
}
}
Use this in textfield delegate method :
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if range.length > 0 {
return true
}
if string == "" {
return false
}
if range.location > 11 {
return false
}
var originalText = textField.text
let replacementText = string.replacingOccurrences(of: " ", with: "")
if !CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: replacementText)) {
return false
}
if range.location == 3 || range.location == 7 {
originalText?.append("-")
textField.text = originalText
}
return true
}
In my application, there is a possibility to add transactions. The transaction has an attribute called amount and this attribute is a Double. I have implemented the function to add a negative and a positive amount. I do this with a UISegmentedControll. If the user makes the amount negative, the amountTextField.textgets to `"-" + amountTextField.text. That the user can input just Doubles I added this function :
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let oldText = textField.text, let r = Range(range, in: oldText) else{
return true
}
let newText = oldText.replacingCharacters(in: r, with: string)
let isNumeric = newText.isEmpty || (Double(newText) != nil)
let numberOfDots = newText.components(separatedBy: ".").count - 1
let numberOfDecimalDigits: Int
if let dotIndex = newText.index(of: "."){
numberOfDecimalDigits = newText.distance(from: dotIndex, to: newText.endIndex) - 1
} else {
numberOfDecimalDigits = 0
}
return isNumeric && numberOfDots <= 1 && numberOfDecimalDigits <= 2
}
When there is a minus in front of the positive Double, it is impossible to delete the first number of the string. For example if the amountTextField.text is -399.99, and the user presses the delete button as often as he wants, the textField will show -3. In my debugging work I found out that the function I added to the code is the reason for this.
How can I fix this issue?
Here is one approach:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let oldText = textField.text, let r = Range(range, in: oldText) else {
return true
}
let newText = oldText.replacingCharacters(in: r, with: string)
if newText == "-" {
// result will be "-" so just
return true
}
let isNumeric = newText.isEmpty || (Double(newText) != nil)
let numberOfDots = newText.components(separatedBy: ".").count - 1
let numberOfDecimalDigits: Int
if let dotIndex = newText.index(of: "."){
numberOfDecimalDigits = newText.distance(from: dotIndex, to: newText.endIndex) - 1
} else {
numberOfDecimalDigits = 0
}
if isNumeric && numberOfDots <= 1 && numberOfDecimalDigits <= 2 {
// value passes those tests, so make sure the leading "-" is still there
// if not, prepend it, set the text and return false
if newText.first != "-" {
textField.text = "-" + newText
return false
}
}
return isNumeric && numberOfDots <= 1 && numberOfDecimalDigits <= 2
}
We have a couple of additional if blocks to handle:
user moves the insertion point to delete the "-"
user does a "select all" and taps a new digit or delete, or pastes a value
I expect you already know you'll also want a bool check to handle this differently if the segmented control is not in the negative position.
I have text label which has phone number in it. I mask the phone number when user is typing so that in shouldChangeCharactersIn function;
I get user input (string)
Add that input to text which is already written in UITextField
Mask text and set it to UITextField
Return false
My question is that after I set text of UITextfield (delete or add new character UITextField, cursor moves to the end but I want it to stay in the same position. (By meaning same position, I mean same when I don't implement shouldChangeCharactersIn function) How can I do that? Thank you.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard CharacterSet(charactersIn: "0123456789").isSuperset(of: CharacterSet(charactersIn: string)) else {
return false
}
if let text = textField.text {
let newLength = text.count + string.count - range.length
let newText = text + string
let textFieldText: NSString = (textField.text ?? "") as NSString
let txtAfterUpdate = textFieldText.replacingCharacters(in: range, with: string)
if(newLength <= 15){
//textField.text = txtAfterUpdate
textField.text = txtAfterUpdate.digits.applyPatternOnNumbers()
return false
}
return newLength <= 15
}
return true
}
Mask Function:
func applyPatternOnNumbers(pattern: String = "(###) ### ## ##", replacmentCharacter: Character = "#") -> String {
var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression)
for index in 0 ..< pattern.count {
guard index < pureNumber.count else { return pureNumber }
let stringIndex = String.Index(encodedOffset: index)
let patternCharacter = pattern[stringIndex]
guard patternCharacter != replacmentCharacter else { continue }
pureNumber.insert(patternCharacter, at: stringIndex)
}
return pureNumber
}
What I want in GIF
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard CharacterSet(charactersIn: "0123456789").isSuperset(of: CharacterSet(charactersIn: string)) else {
return false
}
if let text = textField.text {
let newLength = text.count + string.count - range.length
let newText = text + string
let textFieldText: NSString = (textField.text ?? "") as NSString
let txtAfterUpdate = textFieldText.replacingCharacters(in: range, with: string)
if(newLength <= 15){
var currentPosition = 0
if let selectedRange = textField.selectedTextRange {
currentPosition = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
}
if(string == ""){
if(currentPosition == 2){
if(textField.text!.count > 2){
currentPosition = 1
}else{
currentPosition = 0
}
}else if(currentPosition == 7){
currentPosition = 4
}else if(currentPosition == 11){
currentPosition = 9
}else if(currentPosition == 14){
currentPosition = 12
}else{
if(currentPosition != 1){
currentPosition = currentPosition - 1
}
}
}else{
if(currentPosition == 0){
currentPosition = 2
}else if(currentPosition == 4){
currentPosition = 7
}else if(currentPosition == 9){
currentPosition = 11
}else if(currentPosition == 12){
currentPosition = 14
}else{
currentPosition = currentPosition + 1
}
}
textField.text = txtAfterUpdate.applyPatternOnNumbers()
print("textField Length -> : \(textField.text?.count ?? 0)")
if let newPosition = textField.position(from: textField.beginningOfDocument, offset: currentPosition) {
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
return false
}
return newLength <= 15
}
return true
}
I am trying to format the User input phone number. I have implemented this:
import UIKit
class ViewController: UIViewController {
var phoneNumberTextField : UITextField?
var docController: UIDocumentInteractionController?
override func viewDidLoad() {
super.viewDidLoad()
phoneNumberTextField = UITextField(frame: CGRectMake(10, 150, 100, 50))
phoneNumberTextField?.backgroundColor = UIColor.brownColor()
self.view.addSubview(phoneNumberTextField!)
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
if (textField == phoneNumberTextField)
{
let newString = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
let components = newString.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet)
let decimalString = components.joinWithSeparator("") as NSString
let length = decimalString.length
let hasLeadingOne = length > 0 && decimalString.characterAtIndex(0) == (1 as unichar)
if length == 0 || (length > 10 && !hasLeadingOne) || length > 11
{
let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int
return (newLength > 10) ? false : true
}
var index = 0 as Int
let formattedString = NSMutableString()
if hasLeadingOne
{
formattedString.appendString("1 ")
index += 1
}
if (length - index) > 3
{
let areaCode = decimalString.substringWithRange(NSMakeRange(index, 3))
formattedString.appendFormat("(%#)", areaCode)
index += 3
}
if length - index > 3
{
let prefix = decimalString.substringWithRange(NSMakeRange(index, 3))
formattedString.appendFormat("%#-", prefix)
index += 3
}
let remainder = decimalString.substringFromIndex(index)
formattedString.appendString(remainder)
textField.text = formattedString as String
return false
}
else
{
return true
}
}
}
From Here. When I run the app and I start adding numbers nothing happens. Brackets and hypens do not appear. I want to convert (197)-444-4444 to 1974444444.
What is wrong here?
You dont seems to have set delegate for your textfield:
phoneNumberTextField.delegate = self
And your viewcontroller doesnt seems to conform the UITextFieldDelegate like:
class ViewController: UIViewController, UITextFieldDelegate {
Also, it would never be nil so just change it to var phoneNumberTextField : UITextField!
I have a function for formatting my text field as a phone number, but this function is only working after I save my managed object context. For example, I have a UITableView with static cells for text fields as a contact form. When I'm creating a new contact (before I've saved the contact) the text field for phone number doesn't get formatted by my function, but after I save that contact and reopen it, then go and enter a phone number it gets formatted properly. I'm trying to figure out why this is, and what I am do about it so that the number gets formatted in either case. Here's the function that I'm using to format the phone number.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if textField == phoneTxt {
var newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
var components = newString.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet)
var decimalString = "".join(components) as NSString
var length = decimalString.length
var hasLeadingOne = length > 0 && decimalString.characterAtIndex(0) == (1 as unichar)
if length == 0 || (length > 10 && !hasLeadingOne) || length > 11 {
var newLength = (textField.text as NSString).length + (string as NSString).length - range.length as Int
return (newLength > 10) ? false : true
}
var index = 0 as Int
var formattedString = NSMutableString()
if hasLeadingOne {
formattedString.appendString("1 ")
index += 1
}
if (length - index) > 3 {
var areaCode = decimalString.substringWithRange(NSMakeRange(index, 3))
formattedString.appendFormat("(%#) ", areaCode)
index += 3
}
if length - index > 3 {
var prefix = decimalString.substringWithRange(NSMakeRange(index, 3))
formattedString.appendFormat("%#-", prefix)
index += 3
}
var remainder = decimalString.substringFromIndex(index)
formattedString.appendString(remainder)
textField.text = formattedString
return false
} else {
return true
}
}
`
It seems when the form is used to create a new object, you somehow have not set the delegate of the text field. Check if the posted function is being called at all in this case.