Thanks @Frameworks Engineer
I've decided to rely on using a String with additional validation after all. It seems to me that it should be possible with a double because that's how it needs to be converted and checked to ensure that the string is properly formatted. The important thing is that it works, so here's my imperfect code.
extension Formatter {
static let numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.currencyCode = "GBP"
formatter.currencySymbol = ""
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
formatter.numberStyle = .currency
return formatter
}()
}
struct HomeView: View {
@FocusState private var isFocused: Bool
@State var value: String = ""
var body: some View {
VStack {
TextField("0.0",
text: $value)
.focused($isFocused)
.border(.red)
.keyboardType(.decimalPad)
.onChange(of: isFocused) { newValue in
guard newValue == false else { return }
guard let doubleValue = Double(value) else { return }
value = Formatter.numberFormatter.string(from: NSNumber(floatLiteral: doubleValue)) ?? "0.0"
}
.onReceive(Just(value), perform: { newValue in
if isFocused == false {
return
}
let validator = InputValidator()
guard let validated = validator.validate(newValue: newValue) else {
return
}
if newValue != validated {
value = validated
}
})
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if isFocused {
Spacer()
Button("Done") {
isFocused = false
}
}
}
}
}
}
}
extension HomeView {
struct InputValidator {
func validate(newValue: String) -> String? {
guard let decimalSeparator: Character = Locale.current.decimalSeparator?.first else {
return nil
}
let maxFraction = 2
let filtered = newValue.filter {
$0.isNumber || $0 == decimalSeparator
}
if let fractionIndex = filtered.firstIndex(of: decimalSeparator) {
let distance = filtered.distance(
from: filtered.startIndex,
to: fractionIndex
)
let lastIndex = min(
distance + maxFraction,
filtered.count - 1
)
let arrFiltered = Array(filtered)
let decimal = String(arrFiltered[0 ... distance])
let fraction = String(arrFiltered[distance ... lastIndex].filter({ $0.isNumber }))
return decimal + fraction
}
return filtered
}
}
}
Post
Replies
Boosts
Views
Activity
And next convert this string value in places where we need using this extension
extension String {
var toDoubleWithGroupingSeparator: Double? {
guard let groupingSeparator = Locale.current.groupingSeparator,
let decimalSeparator = Locale.current.decimalSeparator else {
return nil
}
let valueWithSeperator = self.replacingOccurrences(of: groupingSeparator, with: "")
let valueWithFormattedSeperator = valueWithSeperator.replacingOccurrences(of: decimalSeparator, with: ".")
return Double(valueWithFormattedSeperator)
}
}