Reputation: 1630
I am struggling to do this with Swift 3. I have a text field that I would like to limit to only numbers and one decimal point and two characters after the decimal place. I would also like to have it work in regions where a decimal point is not used when entering non-integers. Thank you for any suggestions!
Upvotes: 1
Views: 1974
Reputation: 3845
If you are using Swift UI then the complete solution
File NumbersOnlyViewModifier
import Foundation
import SwiftUI
import Combine
struct NumbersOnlyViewModifier: ViewModifier {
@Binding var text: String
var includeDecimal: Bool
var digitAllowedAfterDecimal: Int = 1
func body(content: Content) -> some View {
content
.keyboardType(includeDecimal ? .decimalPad : .numberPad)
.onReceive(Just(text)) { newValue in
var numbers = "0123456789"
let decimalSeparator: String = Locale.current.decimalSeparator ?? "."
if includeDecimal {
numbers += decimalSeparator
}
if newValue.components(separatedBy: decimalSeparator).count-1 > 1 {
let filtered = newValue
self.text = isValid(newValue: String(filtered.dropLast()), decimalSeparator: decimalSeparator)
} else {
let filtered = newValue.filter { numbers.contains($0)}
if filtered != newValue {
self.text = isValid(newValue: filtered, decimalSeparator: decimalSeparator)
} else {
self.text = isValid(newValue: newValue, decimalSeparator: decimalSeparator)
}
}
}
}
private func isValid(newValue: String, decimalSeparator: String) -> String {
guard includeDecimal, !text.isEmpty else { return newValue }
let component = newValue.components(separatedBy: decimalSeparator)
if component.count > 1 {
guard let last = component.last else { return newValue }
if last.count > digitAllowedAfterDecimal {
let filtered = newValue
return String(filtered.dropLast())
}
}
return newValue
}
}
File View+Extenstion
extension View {
func numbersOnly(_ text: Binding<String>, includeDecimal: Bool = false) -> some View {
self.modifier(NumbersOnlyViewModifier(text: text, includeDecimal: includeDecimal))
}
}
File ViewFile
TextField("\(count, specifier: Constants.FloatFormat.twoDecimalFloat)", text: $value, onEditingChanged: { isEditing in
self.isEditing = isEditing
})
.foregroundColor(Color.neutralGray900)
.numbersOnly($value, includeDecimal: true)
.font(.system(size: Constants.FontSizes.fontSize22))
.multilineTextAlignment(.center)
Upvotes: 0
Reputation: 1791
var number = Double(yourTextfield.text)
if number != nil {
//if user enters more than 2 digits after the decimal point it will round it up to 2
let roundedNumber = Double(num!).roundTo(places: 2)
}
else {
//probably print an error message
}
Upvotes: 0
Reputation: 2802
You need to assign delegate to your textfield and in the shouldChangeCharactersIn delegate method do your validations:
Add extension with validation methods for the string:
extension String{
private static let decimalFormatter:NumberFormatter = {
let formatter = NumberFormatter()
formatter.allowsFloats = true
return formatter
}()
private var decimalSeparator:String{
return String.decimalFormatter.decimalSeparator ?? "."
}
func isValidDecimal(maximumFractionDigits:Int)->Bool{
// Depends on you if you consider empty string as valid number
guard self.isEmpty == false else {
return true
}
// Check if valid decimal
if let _ = String.decimalFormatter.number(from: self){
// Get fraction digits part using separator
let numberComponents = self.components(separatedBy: decimalSeparator)
let fractionDigits = numberComponents.count == 2 ? numberComponents.last ?? "" : ""
return fractionDigits.characters.count <= maximumFractionDigits
}
return false
}
}
In your delegate method:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Get text
let currentText = textField.text ?? ""
let replacementText = (currentText as NSString).replacingCharacters(in: range, with: string)
// Validate
return replacementText.isValidDecimal(maximumFractionDigits: 2)
}
Upvotes: 2