I'm trying to format an expiry field so that it formats the text as follows: MM / YY
I can get the textfield to add in the extra characters when the user is typing however when I come to delete the numbers the code will not allow you to go passed two characters before adding in the " / " again. Is there a way I can recognise when the user is deleting and bypass the text field check?
ExpiryOutlet.addTarget(self, action: #selector(ExpiryDidChange(_:)), for: .editingChanged)
func ExpiryDidChange(_ textField: UITextField) {
if textField == ExpiryOutlet {
if textField.text != "" && textField.text?.characters.count == 2 {
textField.text = "\(textField.text!) / "
}
}
}
Thanks
Is there a way I can recognise when the user is deleting and bypass the text field check?
The problem is that you've implemented the wrong method. Implement the delegate method textField(_:shouldChangeCharactersIn:replacementString:)
. This allows you distinguish where the text is changing (the second parameter is the range), and what the new text is — in the case of the backspace, replacementString
will be empty.
You can subclass UITextField and create a custom field to allow the user to input numbers only by adding a target to your object for controlEvents editingChanged with a selector to update UI.
First lets subclass UITextField:
class ExpirationField: UITextField {
var allowsExpiredDate = false
override func didMoveToSuperview() {
super.didMoveToSuperview()
placeholder = "MM/YY"
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
keyboardType = .numberPad
textAlignment = .center
editingChanged()
}
}
We need also to properly format the field text by filtering all non digits characters converting them to Int using flatMap on the string representation of them and returning an array of Int ranging from 0 to 9. We need to place the slash character depending on the numbers of digits entered by the user by switching the number of digits in the string. Considering the fact that it is an expiration field you will need also to check if the month and year entered by the user still valid. So lets add month and year properties to ExpirationField to return their value. The same applies to the Date so we can compare it to the current month and year to validate the expiration date:
extension ExpirationField {
var string : String { return text ?? "" }
var numbers: [Int] { return string.characters.flatMap{ Int(String($0)) } }
var year: Int { return numbers.suffix(2).integer }
var month: Int { return numbers.prefix(2).integer }
func editingChanged() {
text = expirationFormatted
if text?.characters.count == 5 {
print("Month:", month, "Year:", year, "isValid:", isValid)
if !allowsExpiredDate && !isValid {
text = numbers.prefix(2).string + "/" + numbers.dropLast().suffix(1).string
}
} else {
print("isValid:", false)
switch numbers.count {
case 1 where numbers.integer > 1:
text = ""
case 2 :
if numbers.integer > 12 {
text = "1"
} else if numbers.integer == 0 {
text = "0"
}
case 3 where (numbers.last ?? 0) < 1 && !allowsExpiredDate:
text = numbers.dropLast().string
case 4 where year + 2000 < Date().year && !allowsExpiredDate:
text = numbers.prefix(2).string + "/" + numbers.dropLast().suffix(1).string
default:
break
}
}
if isValid {
layer.borderColor = UIColor.darkGray.cgColor
layer.cornerRadius = 3
layer.borderWidth = 1
} else {
layer.borderColor = UIColor.clear.cgColor
layer.borderWidth = 0
}
}
var expirationFormatted: String {
let numbers = self.numbers.prefix(4)
switch numbers.count {
case 1...2: return numbers.string
case 3: return numbers.prefix(2).string + "/" + numbers.suffix(1).string
case 4: return numbers.prefix(2).string + "/" + numbers.suffix(2).string
default: return ""
}
}
var isValid: Bool {
if string.characters.count < 5 { return false }
guard 1...12 ~= month else {
print("invalid month:", month)
return false
}
guard Date().year-2000...99 ~= year else {
print("invalid year:", year)
return false
}
return year > Date().year-2000 ? true : month >= Date().month
}
override func deleteBackward() {
text = numbers.dropLast().string
text = expirationFormatted
layer.borderColor = UIColor.clear.cgColor
layer.borderWidth = 0
}
}
extension Calendar {
static let iso8601 = Calendar(identifier: .iso8601)
}
extension Date {
var year: Int {
return Calendar.iso8601.component(.year, from: self)
}
var month: Int {
return Calendar.iso8601.component(.month, from: self)
}
}
extension Collection where Iterator.Element == Int {
var string: String {
return map(String.init).joined()
}
var integer: Int { return reduce(0){ 10 * $0 + $1 } }
}
Then You just drag a text field to your view, select it and set custom class to ExpirationField in the inspector:
Sample