I have a UITextView with a custom NSTextStorage, where I add a list bullet, after each Enter, if the prior line starts with a list bullet.
When a user Enter and there is only a list bullet in the beginning of the line where the cursor is, I remove the list bullet and stay on that line.
The first function works as expected. But I have a hard time figuring out how to remove the list bullet.
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) ,0)
replaceCharactersInRange(next, withString: "", selectedRangeLocationMove: text.last!.count)
}
It is the code above that does not work. I probably do not use the correct Range.
This is all of my code.
class TextView: UITextView {
internal var storage: TextStorage!
var defaultAttributes: [NSAttributedString.Key: AnyObject] = [:]
override init(frame: CGRect, textContainer: NSTextContainer?) {
let container = (textContainer == nil) ? NSTextContainer() : textContainer!
container.widthTracksTextView = true
container.heightTracksTextView = true
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(container)
self.storage = TextStorage()
self.storage.addLayoutManager(layoutManager)
super.init(frame: .zero, textContainer: container)
self.textContainerInset = .init(top: 16, left: 16, bottom: 16, right: 16)
self.isScrollEnabled = true
self.storage.textView = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
TextStorage
class TextStorage: NSTextStorage {
var backingStore: NSMutableAttributedString = NSMutableAttributedString()
var textView: UITextView!
override var string: String {
return self.backingStore.string
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
var listPrefix: String? = nil
if (TextUtils.isReturn(str: str)) {
let currentLine = TextUtils.startOffset(self.string, location: range.location).0
let separateds = currentLine.components(separatedBy: " ")
if separateds.first!.contains("•") && currentLine.trimmingCharacters(in: .whitespaces).count == 1 {
listPrefix = ""
}
else {
if separateds.count >= 2 {
if separateds.first!.contains("•") {
listPrefix = "• "
}
}
}
}
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
guard let prefix = listPrefix else {
return
}
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) ,0)
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: text.last!.count)
} else {
let newRange = NSMakeRange(textView.selectedRange.location + str.count, 0)
replaceCharactersInRange(newRange, withString: prefix, selectedRangeLocationMove: prefix.count)
}
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
func replaceCharactersInRange(_ replaceRange: NSRange, withString str: String, selectedRangeLocationMove: Int) {
if textView.undoManager!.isUndoing {
textView.selectedRange = NSMakeRange(textView.selectedRange.location - selectedRangeLocationMove, 0)
replaceCharactersInRange(NSMakeRange(replaceRange.location, str.count), withString: "")
} else {
replaceCharactersInRange(replaceRange, withString: str)
textView.selectedRange = NSMakeRange(textView.selectedRange.location + selectedRangeLocationMove, 0)
}
}
}
extension NSMutableAttributedString {
func replaceCharactersInRange(_ range: NSRange, withString str: String) {
if isSafeRange(range) {
replaceCharacters(in: range, with: str)
}
}
func isSafeRange(_ range: NSRange) -> Bool {
if range.location < 0 {
return false
}
let maxLength = range.location + range.length
return maxLength <= string.count
}
}
class TextUtils {
class func isReturn(str: String) -> Bool {
return str == "\n"
}
class func isBackspace(str: String) -> Bool {
return str == ""
}
class func startOffset(_ string: String, location: Int) -> (String, Int) {
var offset: Int = 0
var word = NSString(string: string).substring(to: location)
let lines = string.components(separatedBy: "\n")
if lines.count > 0 {
let last = lines.last!
offset = word.count - last.count
word = last
}
return (word, offset)
}
}
I used also used self.deleteCharacters(in:) instead of the following function. It did not work either.
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: text.last!.count)
To sum up, I just need to remove a word where the cursor is when a user Enter and stay on that line.
I will be very grateful if you help me fix this issue.
Update
How to test this code?
On the first line, add a bullet + one space [• ], write a few words and enter.
From this point on, it adds a bullet since the prior line starts with a bullet.
Find below modified functions. Tested with Xcode 11.4 / iOS 13.4
override func replaceCharacters(in range: NSRange, with str: String) {
var listPrefix: String? = nil
if (TextUtils.isReturn(str: str)) {
let currentLine = TextUtils.startOffset(self.string, location: range.location).0
let separateds = currentLine.components(separatedBy: " ")
if separateds.first!.contains("•") && currentLine.trimmingCharacters(in: .whitespaces).count == 1 {
listPrefix = ""
}
else {
if separateds.count >= 2 {
if separateds.first!.contains("•") {
listPrefix = "• "
}
}
}
}
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
guard let prefix = listPrefix else {
return
}
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) - 1, text.last!.count + 1)
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: 0)
} else {
let newRange = NSMakeRange(textView.selectedRange.location + str.count, 0)
replaceCharactersInRange(newRange, withString: prefix, selectedRangeLocationMove: prefix.count)
}
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
guard range.upperBound <= string.count else { return }
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
Related
I have a number let’s say 0.00.
When the user taps 1. We should have 0.01
When the user taps 2. We should display 0.12
When the user taps 3. We should display 1.23
When the user taps 4. We should display 12.34
How can I do that with Swift?
For Swift 3. Input currency format on a text field (from right to left)
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(myTextFieldDidChange), for: .editingChanged)
}
#objc func myTextFieldDidChange(_ textField: UITextField) {
if let amountString = textField.text?.currencyInputFormatting() {
textField.text = amountString
}
}
extension String {
// formatting text for currency textField
func currencyInputFormatting() -> String {
var number: NSNumber!
let formatter = NumberFormatter()
formatter.numberStyle = .currencyAccounting
formatter.currencySymbol = "$"
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
var amountWithPrefix = self
// remove from String: "$", ".", ","
let regex = try! NSRegularExpression(pattern: "[^0-9]", options: .caseInsensitive)
amountWithPrefix = regex.stringByReplacingMatches(in: amountWithPrefix, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, self.count), withTemplate: "")
let double = (amountWithPrefix as NSString).doubleValue
number = NSNumber(value: (double / 100))
// if first number is 0 or all numbers were deleted
guard number != 0 as NSNumber else {
return ""
}
return formatter.string(from: number)!
}
}
You can create a currency text field subclassing UITextField. Add a target for UIControlEvents .editingChanged. Add a selector method to filter the digits from your textfield string. After filtering all non digits from your string you can format again your number using NumberFormatter as follow:
Xcode 11.5 • Swift 5.2 or later
import UIKit
class CurrencyField: UITextField {
var decimal: Decimal { string.decimal / pow(10, Formatter.currency.maximumFractionDigits) }
var maximum: Decimal = 999_999_999.99
private var lastValue: String?
var locale: Locale = .current {
didSet {
Formatter.currency.locale = locale
sendActions(for: .editingChanged)
}
}
override func willMove(toSuperview newSuperview: UIView?) {
// you can make it a fixed locale currency if needed
// self.locale = Locale(identifier: "pt_BR") // or "en_US", "fr_FR", etc
Formatter.currency.locale = locale
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
keyboardType = .numberPad
textAlignment = .right
sendActions(for: .editingChanged)
}
override func deleteBackward() {
text = string.digits.dropLast().string
// manually send the editingChanged event
sendActions(for: .editingChanged)
}
#objc func editingChanged() {
guard decimal <= maximum else {
text = lastValue
return
}
text = decimal.currency
lastValue = text
}
}
extension CurrencyField {
var doubleValue: Double { (decimal as NSDecimalNumber).doubleValue }
}
extension UITextField {
var string: String { text ?? "" }
}
extension NumberFormatter {
convenience init(numberStyle: Style) {
self.init()
self.numberStyle = numberStyle
}
}
private extension Formatter {
static let currency: NumberFormatter = .init(numberStyle: .currency)
}
extension StringProtocol where Self: RangeReplaceableCollection {
var digits: Self { filter (\.isWholeNumber) }
}
extension String {
var decimal: Decimal { Decimal(string: digits) ?? 0 }
}
extension Decimal {
var currency: String { Formatter.currency.string(for: self) ?? "" }
}
extension LosslessStringConvertible {
var string: String { .init(self) }
}
View Controller
class ViewController: UIViewController {
#IBOutlet weak var currencyField: CurrencyField!
override func viewDidLoad() {
super.viewDidLoad()
currencyField.addTarget(self, action: #selector(currencyFieldChanged), for: .editingChanged)
currencyField.locale = Locale(identifier: "pt_BR") // or "en_US", "fr_FR", etc
}
#objc func currencyFieldChanged() {
print("currencyField:",currencyField.text!)
print("decimal:", currencyField.decimal)
print("doubleValue:",(currencyField.decimal as NSDecimalNumber).doubleValue, terminator: "\n\n")
}
}
Sample project
SwiftUI version of this post here
I started with Leo Dabus' answer (which didn't work out of the box for me) and in the process of trying to simplify and make it work ended up with this, which I think is pretty lean & clean if I do say so myself 😎
class CurrencyTextField: UITextField {
/// The numbers that have been entered in the text field
private var enteredNumbers = ""
private var didBackspace = false
var locale: Locale = .current
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
}
override func deleteBackward() {
enteredNumbers = String(enteredNumbers.dropLast())
text = enteredNumbers.asCurrency(locale: locale)
// Call super so that the .editingChanged event gets fired, but we need to handle it differently, so we set the `didBackspace` flag first
didBackspace = true
super.deleteBackward()
}
#objc func editingChanged() {
defer {
didBackspace = false
text = enteredNumbers.asCurrency(locale: locale)
}
guard didBackspace == false else { return }
if let lastEnteredCharacter = text?.last, lastEnteredCharacter.isNumber {
enteredNumbers.append(lastEnteredCharacter)
}
}
}
private extension Formatter {
static let currency: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
}
private extension String {
func asCurrency(locale: Locale) -> String? {
Formatter.currency.locale = locale
if self.isEmpty {
return Formatter.currency.string(from: NSNumber(value: 0))
} else {
return Formatter.currency.string(from: NSNumber(value: (Double(self) ?? 0) / 100))
}
}
}
Try this piece of code:
struct DotNum {
private var fraction:String = ""
private var intval:String = ""
init() {}
mutating func enter(s:String) {
if count(fraction) < 2 {
fraction = s + fraction
} else {
intval = s + intval
}
}
private var sFract:String {
if count(fraction) == 0 { return "00" }
if count(fraction) == 1 { return "0\(fraction)" }
return fraction
}
var stringVal:String {
if intval == "" { return "0.\(sFract)" }
return "\(intval).\(sFract)"
}
}
var val = DotNum()
val.enter("1")
val.stringVal
val.enter("2")
val.stringVal
val.enter("3")
val.stringVal
val.enter("4")
val.stringVal
My final code thanks for your help
extension Double {
var twoDigits: Double {
let nf = NSNumberFormatter()
nf.numberStyle = NSNumberFormatterStyle.DecimalStyle
nf.minimumFractionDigits = 2
nf.maximumFractionDigits = 2
return self
}
}
var cleanText:String!
let number:String = sender.currentTitle as String!
if(amountDisplay.text != nil)
{
cleanText = String(Array(amountDisplay.text!).map{String($0)}.filter{ $0.toInt() != nil }.map{Character($0)} ) as String
cleanText = cleanText + number
}else{
cleanText = number
}
amount = (Double(cleanText.toInt()!) / 100).twoDigits
formatter.locale = NSLocale(localeIdentifier: currencies[current_currency_index])
amountDisplay.text = "\(formatter.stringFromNumber(amount!)!)"
Here is a code for swift 2
#IBOutlet weak var txtAmount: UITextField!
//MARK: - UITextField Delegate -
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool{
if string.characters.count == 0 {
return true
}
let userEnteredString = textField.text ?? ""
var newString = (userEnteredString as NSString).stringByReplacingCharactersInRange(range, withString: string) as NSString
newString = newString.stringByReplacingOccurrencesOfString(".", withString: "")
let centAmount : NSInteger = newString.integerValue
let amount = (Double(centAmount) / 100.0)
if newString.length < 16 {
let str = String(format: "%0.2f", arguments: [amount])
txtAmount.text = str
}
return false //return false for exact out put
}
Note : Connect delegate for textField from storyboard or programatically
Just for fun: copied Thomas's answer (full credits -and points- to him please) into a file to run as a Swift 4.1 script (with minor fixes):
dotnum.swift:
#!/usr/bin/swift
struct DotNum {
private var fraction:String = ""
private var intval:String = ""
init() {}
mutating func enter(_ s:String) {
if fraction.count < 2 {
fraction = s + fraction
} else {
intval = s + intval
}
}
private var sFract:String {
if fraction.count == 0 { return "00" }
if fraction.count == 1 { return "0\(fraction)" }
return fraction
}
var stringVal:String {
if intval == "" { return "0.\(sFract)" }
return "\(intval).\(sFract)"
}
}
var val = DotNum()
val.enter("1")
print(val.stringVal)
val.enter("2")
print(val.stringVal)
val.enter("3")
print(val.stringVal)
val.enter("4")
print(val.stringVal)
Then run it in a terminal:
$ chmod +x dotnum.swift
$ ./dotnum.swift
0.01
0.21
3.21
43.21
Thanks to everyone here. From all the answers here I managed to come out with mine.
First I set up the initial value of the textField to be:
private func commonInit() {
amountTextField.text = "0.00"
}
Then I use the UITextFieldDelegate to get the input value and the current textview.text:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//Need to check if the textfield.text can be evaluated as number or not before passing it to the function
//Get the current text value, and current user input and pass it to the
let formattedAmount = formatAmount(oldAmount: textField.text, userInput: string)
textField.text = formattedAmount
return false
}
Here go my private function to format the number to move from right to left:
private func formatAmount(currentText: String, userInput: String) -> String {
let amount = currentText.components(separatedBy: ".")
var intValue: String = amount[0]
var decimalValue: String = amount[1]
//backspace registered, need to move the number to the right
if userInput.isEmpty {
decimalValue.remove(at: decimalValue.index(before: decimalValue.endIndex))
decimalValue = intValue.last!.string + decimalValue
intValue.remove(at: intValue.index(before: intValue.endIndex))
if intValue.isEmpty {
intValue = "0"
}
} else {
//Need to consider if user paste value
if userInput.count > 2 {
decimalValue = String(userInput.suffix(2))
intValue = String(userInput.dropLast(2))
} else {
decimalValue = rmAmount[1] + userInput
//Add to int value (move to the right)
intValue = intValue + decimalValue.first!.string
if Int(intValue) == 0 {
intValue = "0" //00 -> 0
} else if intValue.first == "0" {
//remove 0 from at the first position in intValue
intValue.remove(at: intValue.startIndex) //01 -> 1
}
//Remove tenth place from decimal value since it goes to Int already
decimalValue.remove(at: decimalValue.startIndex)
}
}
return intValue + "." + decimalValue
}
This is basically it. Other extra implementations can be added by your own initiatives. Let me know if there is any problem with my implementation.
PS: This is of course only works for certain currency only, in my case, my apps is set up only for that local so thats why I use this way.
After a lot of trial and error with the suggested answers, I found a pretty straight forward solution:
The setup for the textField needs to be called in your view's setup.
In the switch statement, if the user puts in a number between 0 and 9, the number is added to the previous string value. The default case covers the backspace button and removes the last character from the string.
The locale for the numberFormatter is set to current, so it works with different currencies.
func setupTextField() {
textField.delegate = self
textField.tintColor = .clear
textField.keyboardType = .numberPad
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
setFormattedAmount(string)
return false
}
private func setFormattedAmount(_ string: String) {
switch string {
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
amountString = amountString + string
default:
if amountString.count > 0 {
amountString.removeLast()
}
}
let amount = (NSString(string: amountString).doubleValue) / 100
textField.text = formatAmount(amount)
}
private func formatAmount(_ amount: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = .current
if let amount = formatter.string(from: NSNumber(value: amount)) {
return amount
}
return ""
}
I have one text field with place holder called letschat. Now whenever I start typing in my textfield, I want to show my textfield as some #letschat. When my textfield is empty that time my placeholder have to show. That I did. But I want to set whenever I start typing in my textfield. Whatever I am typing with that I want this text also to visible like:
Some #Lletschat
How can I do this?
I created a UITextField subclass that uses the placeholder (if set) as a suffix. As far as I can see everything works as expected. Maybe there are some tweaks needed to suit your needs.
Feel free to ask if anything is unclear:
class SuffixTextField: UITextField {
override init(frame: CGRect) {
super.init(frame: frame)
sharedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
sharedInit()
}
private func sharedInit() {
addTarget(self, action: #selector(textChanged), for: .editingChanged)
}
override var text: String? {
didSet {
selectedTextRange = maxTextRange
}
}
override var attributedText: NSAttributedString? {
didSet {
selectedTextRange = maxTextRange
}
}
#objc private func textChanged() {
if let currentText = text, let placeholder = placeholder {
if currentText == placeholder {
self.text = nil
} else if !currentText.hasSuffix(placeholder) {
self.text = currentText + placeholder
}
}
}
private var maxCursorPosition: UITextPosition? {
guard let placeholder = placeholder, !placeholder.isEmpty else { return nil }
guard let text = text, !text.isEmpty else { return nil }
return position(from: beginningOfDocument, offset: (text as NSString).range(of: placeholder, options: .backwards).location)
}
private var maxTextRange: UITextRange? {
guard let maxCursorPosition = maxCursorPosition else { return nil }
return textRange(from: maxCursorPosition, to: maxCursorPosition)
}
override var selectedTextRange: UITextRange? {
get { return super.selectedTextRange }
set {
guard let newRange = newValue,
let maxCursorPosition = maxCursorPosition else {
super.selectedTextRange = newValue
return
}
if compare(maxCursorPosition, to: newRange.start) == .orderedAscending {
super.selectedTextRange = textRange(from: maxCursorPosition, to: maxCursorPosition)
} else if compare(maxCursorPosition, to: newRange.end) == .orderedAscending {
super.selectedTextRange = textRange(from: newRange.start, to: maxCursorPosition)
} else {
super.selectedTextRange = newValue
}
}
}
}
here you can see a preview:
https://www.dropbox.com/s/etkbme37wuxbw1q/preview.mov?dl=0
You can take the action of UITextField textFieldDidChange and get the call of every time when the textField text changed.
Just like that:
func textChangedAction(sender:UITextFiled) {
if sender.text.rangeOfString("#Lletschat") != nil{
sender.text = sender.text.replacingOccurrences(of: "#Lletschat", with: "")
}
sender.text = "\(sender.text!) #Lletschat"
}
If you want to changed the color of your specific text you can check here.
Implement textfield delegate like this-
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
textField.text = textField.text?.replacingOccurrences(of: " #\(textField.placeholder!)", with: "", options: .literal, range: nil)
textField.text?.append(string)
textField.text?.append(" #\(textField.placeholder!)")
return false
}
Simple solution:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let newRange = textField.text?.range(from: range), let result = textField.text?.replacingCharacters(in: newRange, with: string) else { return true }
if result.endsWithString("#letschat") {
return true
} else {
textField.text = result + "#letschat"
let position = textField.position(from: textField.beginningOfDocument, offset: result.characters.count)!
textField.selectedTextRange = textField.textRange(from: position, to: position)
return false
}
}
With helper extension:
extension String {
func range(from oldOne: NSRange) -> Range<String.Index>? {
guard
let from16 = utf16.index(utf16.startIndex, offsetBy: oldOne.location, limitedBy: utf16.endIndex),
let to16 = utf16.index(utf16.startIndex, offsetBy: oldOne.location + oldOne.length, limitedBy: utf16.endIndex),
let from = from16.samePosition(in: self),
let to = to16.samePosition(in: self)
else { return nil }
return from ..< to
}
func endsWithString(_ string: String) -> Bool {
guard characters.count >= string.characters.count else { return false }
let index = self.index(startIndex, offsetBy: characters.count - string.characters.count)
let substring = self.substring(from: index)
return substring == string
}
}
Difficult but clear solution is to create your own UIControl-subclass with UITextField and UILabel children views:
+-----------+ +-----------+
| textfield | -(3px)- | #letschat |
+-----------+ +-----------+
Use autolayout to keep the distance of 3 pixels between it.
Don't forget to configure your class to send all the incoming actions to the textfield. You can use different font colours for these controls so user won't be confused about efforts to change label's value.
Conform your class to UITextfieldDelegate and then assign textfield.delegate = self
Now, add this Delegate Method, if you want your #letschat to be appended after user endtyping.
func textFieldDidEndEditing(textField: UITextField) {
textField.text = "\(textField.text)#letschat"
}
or if you want it at the typing time.
textField.addTarget(self, action: #selector(YourViewController.textFieldDidChange(_:)), forControlEvents: UIControlEvents.EditingChanged)
func textFieldDidChange(textField: UITextField) {
if textField.containsString("#letschat") {
textField.text = textField.text.stringByReplacingOccurrencesOfString("#letschat", withString: "")
}
textField.text = "\(textField.text)#letschat"
}
Hope this helps.
I created textField and want to format text in it like 43/35. Number / number - for credit card month and year.
Can I use number fomatter for it or how can I do it more easily?
The issue here that I need to replace 3rd character if I add new character and remove it if I remove 2nd one.
I do not want use any 3rd party library, I need native implementation
This is my current solution. Basically you need to:
1) Implement the delegate of your textfield somewhere (in my code below I implemented on the ViewController)
2) Implement textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool.
To apply the mask, I created some extensions for String and Characters, as you can see at the end of the following code:
class ViewController: UIViewController {
#IBOutlet weak var textfield: UITextField!
let mask = "##/##"
override func viewDidLoad() {
super.viewDidLoad()
textfield.delegate = self
}
}
extension ViewController: UITextFieldDelegate {
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
guard let normalText = textField.text else { return false }
let beginning = textField.beginningOfDocument
// save cursor location
let cursorLocation = textField.positionFromPosition(beginning, offset: range.location + string.characters.count)
let newString = (normalText as NSString).stringByReplacingCharactersInRange(range, withString: string)
let newStringClean = newString.stringWithOnlyNumbers().withMask(mask)
guard newString != newStringClean else { return true }
textField.text = newStringClean
guard string != "" else { return false }
// fix cursor location after changing textfield.text
if let cL = cursorLocation {
let textRange = textField.textRangeFromPosition(cL, toPosition: cL)
textField.selectedTextRange = textRange
}
return false
}
}
extension String {
func stringWithOnlyNumbers() -> String {
return self.characters.reduce("") { (acc, c) -> String in
guard c.isDigit() else { return acc }
return "\(acc)\(c)"
}
}
func withMask(mask: String) -> String {
var resultString = String()
let chars = self.characters
let maskChars = mask.characters
var stringIndex = chars.startIndex
var maskIndex = mask.startIndex
while stringIndex < chars.endIndex && maskIndex < maskChars.endIndex {
if (maskChars[maskIndex] == "#") {
resultString.append(chars[stringIndex])
stringIndex = stringIndex.successor()
} else {
resultString.append(maskChars[maskIndex])
}
maskIndex = maskIndex.successor()
}
return resultString
}
}
extension Character {
func isDigit() -> Bool {
let s = String(self).unicodeScalars
let uni = s[s.startIndex]
let digits = NSCharacterSet.decimalDigitCharacterSet()
let isADigit = digits.longCharacterIsMember(uni.value)
return isADigit
}
}
Swift 4:
extension CodeEnterViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let normalText = textField.text else { return false }
let beginning = textField.beginningOfDocument
// save cursor location
let cursorLocation = textField.position(from: beginning, offset: range.location + string.count)
let newString = (normalText as NSString).replacingCharacters(in: range, with: string)
let newStringClean = newString.stringWithOnlyNumbers().withMask(mask: mask)
guard newString != newStringClean else { return true }
textField.text = newStringClean
guard string != "" else { return false }
// fix cursor location after changing textfield.text
if let cL = cursorLocation {
let textRange = textField.textRange(from: cL, to: cL)
textField.selectedTextRange = textRange
}
return false
}
}
String
extension String {
func stringWithOnlyNumbers() -> String {
return self.reduce("") { (acc, c) -> String in
guard c.isDigit() else { return acc }
return "\(acc)\(c)"
}
}
func withMask(mask: String) -> String {
var resultString = String()
let chars = self
let maskChars = mask
var stringIndex = chars.startIndex
var maskIndex = mask.startIndex
while stringIndex < chars.endIndex && maskIndex < maskChars.endIndex {
if (maskChars[maskIndex] == "#") {
resultString.append(chars[stringIndex])
stringIndex = chars.index(after: stringIndex)
} else {
resultString.append(maskChars[maskIndex])
}
maskIndex = chars.index(after: maskIndex)
}
return resultString
}
}
Character
extension Character {
func isDigit() -> Bool {
let s = String(self).unicodeScalars
let uni = s[s.startIndex]
let digits = NSCharacterSet.decimalDigits
let isADigit = digits.hasMember(inPlane: UInt8(uni.value))
return isADigit
} }
i try matchesForRegexInText and number only success. but I'm want Double matches ...
example
(o) inputText : 1.00
(x) inputText : 1.0.0
(x) inputText : .01
i trying code
inputtexts.addTarget(self, action: #selector(tfta(_:)), forControlEvents: UIControlEvents.EditingChanged) // text field changed event
func tfta(textField: UITextField) {
let matches: [String] = matchesForRegexInText("[0-9]", text: textField.text)
textField.text = arrayToString(matches)
}
func matchesForRegexInText(regex: String!, text: String!) -> [String] {
do {
let regex = try NSRegularExpression(pattern: regex, options: [])
let nsString = text as NSString
let results = regex.matchesInString(text, options: [], range: NSMakeRange(0, nsString.length))
return results.map { nsString.substringWithRange($0.range)}
} catch let error as NSError {
print("invalid regex: \(error.localizedDescription)")
return []
}
}
func arrayToString(strArr: [String]) -> String{
var result = ""
if strArr.count > 0 {
for i in 0 ..< strArr.count {
result += strArr[i]
}
}
return result
}
I have a number let’s say 0.00.
When the user taps 1. We should have 0.01
When the user taps 2. We should display 0.12
When the user taps 3. We should display 1.23
When the user taps 4. We should display 12.34
How can I do that with Swift?
For Swift 3. Input currency format on a text field (from right to left)
override func viewDidLoad() {
super.viewDidLoad()
textField.addTarget(self, action: #selector(myTextFieldDidChange), for: .editingChanged)
}
#objc func myTextFieldDidChange(_ textField: UITextField) {
if let amountString = textField.text?.currencyInputFormatting() {
textField.text = amountString
}
}
extension String {
// formatting text for currency textField
func currencyInputFormatting() -> String {
var number: NSNumber!
let formatter = NumberFormatter()
formatter.numberStyle = .currencyAccounting
formatter.currencySymbol = "$"
formatter.maximumFractionDigits = 2
formatter.minimumFractionDigits = 2
var amountWithPrefix = self
// remove from String: "$", ".", ","
let regex = try! NSRegularExpression(pattern: "[^0-9]", options: .caseInsensitive)
amountWithPrefix = regex.stringByReplacingMatches(in: amountWithPrefix, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, self.count), withTemplate: "")
let double = (amountWithPrefix as NSString).doubleValue
number = NSNumber(value: (double / 100))
// if first number is 0 or all numbers were deleted
guard number != 0 as NSNumber else {
return ""
}
return formatter.string(from: number)!
}
}
You can create a currency text field subclassing UITextField. Add a target for UIControlEvents .editingChanged. Add a selector method to filter the digits from your textfield string. After filtering all non digits from your string you can format again your number using NumberFormatter as follow:
Xcode 11.5 • Swift 5.2 or later
import UIKit
class CurrencyField: UITextField {
var decimal: Decimal { string.decimal / pow(10, Formatter.currency.maximumFractionDigits) }
var maximum: Decimal = 999_999_999.99
private var lastValue: String?
var locale: Locale = .current {
didSet {
Formatter.currency.locale = locale
sendActions(for: .editingChanged)
}
}
override func willMove(toSuperview newSuperview: UIView?) {
// you can make it a fixed locale currency if needed
// self.locale = Locale(identifier: "pt_BR") // or "en_US", "fr_FR", etc
Formatter.currency.locale = locale
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
keyboardType = .numberPad
textAlignment = .right
sendActions(for: .editingChanged)
}
override func deleteBackward() {
text = string.digits.dropLast().string
// manually send the editingChanged event
sendActions(for: .editingChanged)
}
#objc func editingChanged() {
guard decimal <= maximum else {
text = lastValue
return
}
text = decimal.currency
lastValue = text
}
}
extension CurrencyField {
var doubleValue: Double { (decimal as NSDecimalNumber).doubleValue }
}
extension UITextField {
var string: String { text ?? "" }
}
extension NumberFormatter {
convenience init(numberStyle: Style) {
self.init()
self.numberStyle = numberStyle
}
}
private extension Formatter {
static let currency: NumberFormatter = .init(numberStyle: .currency)
}
extension StringProtocol where Self: RangeReplaceableCollection {
var digits: Self { filter (\.isWholeNumber) }
}
extension String {
var decimal: Decimal { Decimal(string: digits) ?? 0 }
}
extension Decimal {
var currency: String { Formatter.currency.string(for: self) ?? "" }
}
extension LosslessStringConvertible {
var string: String { .init(self) }
}
View Controller
class ViewController: UIViewController {
#IBOutlet weak var currencyField: CurrencyField!
override func viewDidLoad() {
super.viewDidLoad()
currencyField.addTarget(self, action: #selector(currencyFieldChanged), for: .editingChanged)
currencyField.locale = Locale(identifier: "pt_BR") // or "en_US", "fr_FR", etc
}
#objc func currencyFieldChanged() {
print("currencyField:",currencyField.text!)
print("decimal:", currencyField.decimal)
print("doubleValue:",(currencyField.decimal as NSDecimalNumber).doubleValue, terminator: "\n\n")
}
}
Sample project
SwiftUI version of this post here
I started with Leo Dabus' answer (which didn't work out of the box for me) and in the process of trying to simplify and make it work ended up with this, which I think is pretty lean & clean if I do say so myself 😎
class CurrencyTextField: UITextField {
/// The numbers that have been entered in the text field
private var enteredNumbers = ""
private var didBackspace = false
var locale: Locale = .current
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
}
override func deleteBackward() {
enteredNumbers = String(enteredNumbers.dropLast())
text = enteredNumbers.asCurrency(locale: locale)
// Call super so that the .editingChanged event gets fired, but we need to handle it differently, so we set the `didBackspace` flag first
didBackspace = true
super.deleteBackward()
}
#objc func editingChanged() {
defer {
didBackspace = false
text = enteredNumbers.asCurrency(locale: locale)
}
guard didBackspace == false else { return }
if let lastEnteredCharacter = text?.last, lastEnteredCharacter.isNumber {
enteredNumbers.append(lastEnteredCharacter)
}
}
}
private extension Formatter {
static let currency: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
}
private extension String {
func asCurrency(locale: Locale) -> String? {
Formatter.currency.locale = locale
if self.isEmpty {
return Formatter.currency.string(from: NSNumber(value: 0))
} else {
return Formatter.currency.string(from: NSNumber(value: (Double(self) ?? 0) / 100))
}
}
}
Try this piece of code:
struct DotNum {
private var fraction:String = ""
private var intval:String = ""
init() {}
mutating func enter(s:String) {
if count(fraction) < 2 {
fraction = s + fraction
} else {
intval = s + intval
}
}
private var sFract:String {
if count(fraction) == 0 { return "00" }
if count(fraction) == 1 { return "0\(fraction)" }
return fraction
}
var stringVal:String {
if intval == "" { return "0.\(sFract)" }
return "\(intval).\(sFract)"
}
}
var val = DotNum()
val.enter("1")
val.stringVal
val.enter("2")
val.stringVal
val.enter("3")
val.stringVal
val.enter("4")
val.stringVal
My final code thanks for your help
extension Double {
var twoDigits: Double {
let nf = NSNumberFormatter()
nf.numberStyle = NSNumberFormatterStyle.DecimalStyle
nf.minimumFractionDigits = 2
nf.maximumFractionDigits = 2
return self
}
}
var cleanText:String!
let number:String = sender.currentTitle as String!
if(amountDisplay.text != nil)
{
cleanText = String(Array(amountDisplay.text!).map{String($0)}.filter{ $0.toInt() != nil }.map{Character($0)} ) as String
cleanText = cleanText + number
}else{
cleanText = number
}
amount = (Double(cleanText.toInt()!) / 100).twoDigits
formatter.locale = NSLocale(localeIdentifier: currencies[current_currency_index])
amountDisplay.text = "\(formatter.stringFromNumber(amount!)!)"
Here is a code for swift 2
#IBOutlet weak var txtAmount: UITextField!
//MARK: - UITextField Delegate -
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool{
if string.characters.count == 0 {
return true
}
let userEnteredString = textField.text ?? ""
var newString = (userEnteredString as NSString).stringByReplacingCharactersInRange(range, withString: string) as NSString
newString = newString.stringByReplacingOccurrencesOfString(".", withString: "")
let centAmount : NSInteger = newString.integerValue
let amount = (Double(centAmount) / 100.0)
if newString.length < 16 {
let str = String(format: "%0.2f", arguments: [amount])
txtAmount.text = str
}
return false //return false for exact out put
}
Note : Connect delegate for textField from storyboard or programatically
Just for fun: copied Thomas's answer (full credits -and points- to him please) into a file to run as a Swift 4.1 script (with minor fixes):
dotnum.swift:
#!/usr/bin/swift
struct DotNum {
private var fraction:String = ""
private var intval:String = ""
init() {}
mutating func enter(_ s:String) {
if fraction.count < 2 {
fraction = s + fraction
} else {
intval = s + intval
}
}
private var sFract:String {
if fraction.count == 0 { return "00" }
if fraction.count == 1 { return "0\(fraction)" }
return fraction
}
var stringVal:String {
if intval == "" { return "0.\(sFract)" }
return "\(intval).\(sFract)"
}
}
var val = DotNum()
val.enter("1")
print(val.stringVal)
val.enter("2")
print(val.stringVal)
val.enter("3")
print(val.stringVal)
val.enter("4")
print(val.stringVal)
Then run it in a terminal:
$ chmod +x dotnum.swift
$ ./dotnum.swift
0.01
0.21
3.21
43.21
Thanks to everyone here. From all the answers here I managed to come out with mine.
First I set up the initial value of the textField to be:
private func commonInit() {
amountTextField.text = "0.00"
}
Then I use the UITextFieldDelegate to get the input value and the current textview.text:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//Need to check if the textfield.text can be evaluated as number or not before passing it to the function
//Get the current text value, and current user input and pass it to the
let formattedAmount = formatAmount(oldAmount: textField.text, userInput: string)
textField.text = formattedAmount
return false
}
Here go my private function to format the number to move from right to left:
private func formatAmount(currentText: String, userInput: String) -> String {
let amount = currentText.components(separatedBy: ".")
var intValue: String = amount[0]
var decimalValue: String = amount[1]
//backspace registered, need to move the number to the right
if userInput.isEmpty {
decimalValue.remove(at: decimalValue.index(before: decimalValue.endIndex))
decimalValue = intValue.last!.string + decimalValue
intValue.remove(at: intValue.index(before: intValue.endIndex))
if intValue.isEmpty {
intValue = "0"
}
} else {
//Need to consider if user paste value
if userInput.count > 2 {
decimalValue = String(userInput.suffix(2))
intValue = String(userInput.dropLast(2))
} else {
decimalValue = rmAmount[1] + userInput
//Add to int value (move to the right)
intValue = intValue + decimalValue.first!.string
if Int(intValue) == 0 {
intValue = "0" //00 -> 0
} else if intValue.first == "0" {
//remove 0 from at the first position in intValue
intValue.remove(at: intValue.startIndex) //01 -> 1
}
//Remove tenth place from decimal value since it goes to Int already
decimalValue.remove(at: decimalValue.startIndex)
}
}
return intValue + "." + decimalValue
}
This is basically it. Other extra implementations can be added by your own initiatives. Let me know if there is any problem with my implementation.
PS: This is of course only works for certain currency only, in my case, my apps is set up only for that local so thats why I use this way.
After a lot of trial and error with the suggested answers, I found a pretty straight forward solution:
The setup for the textField needs to be called in your view's setup.
In the switch statement, if the user puts in a number between 0 and 9, the number is added to the previous string value. The default case covers the backspace button and removes the last character from the string.
The locale for the numberFormatter is set to current, so it works with different currencies.
func setupTextField() {
textField.delegate = self
textField.tintColor = .clear
textField.keyboardType = .numberPad
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
setFormattedAmount(string)
return false
}
private func setFormattedAmount(_ string: String) {
switch string {
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
amountString = amountString + string
default:
if amountString.count > 0 {
amountString.removeLast()
}
}
let amount = (NSString(string: amountString).doubleValue) / 100
textField.text = formatAmount(amount)
}
private func formatAmount(_ amount: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = .current
if let amount = formatter.string(from: NSNumber(value: amount)) {
return amount
}
return ""
}