Skip to content

Instantly share code, notes, and snippets.

@pitt500
Created November 1, 2025 22:34
Show Gist options
  • Save pitt500/8d7ec173418fca024dbbff132a0d8c5b to your computer and use it in GitHub Desktop.
Save pitt500/8d7ec173418fca024dbbff132a0d8c5b to your computer and use it in GitHub Desktop.
//
// ContentView.swift
// CommaVsPeriodDemo
//
// Created by Pedro Rojas on 18/09/25.
//
import SwiftUI
struct DecimalTextField: View {
let title: String
@Binding var text: String
let locale = Locale.current
var body: some View {
TextField(title, text: $text)
.keyboardType(.decimalPad)
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.padding(10)
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
.onChange(of: text) { _, newValue in
let sanitizedDigits = sanitizeDigits(newValue, locale: locale)
if sanitizedDigits != newValue {
text = sanitizedDigits
}
}
}
private func sanitizeDigits(_ s: String, locale: Locale) -> String {
var result = ""
var seenDecimalSeparator = false
let decimalSeparator = Character(locale.decimalSeparator ?? ".")
// Keep Digits and just one ".". It will discard everything else.
for ch in s {
if ch.isNumber {
result.append(ch)
} else if ch == decimalSeparator && !seenDecimalSeparator {
result.append(decimalSeparator)
seenDecimalSeparator = true
}
}
// Split Integers and fractional parts from "."
let parts = result.split(
separator: decimalSeparator,
maxSplits: 1,
omittingEmptySubsequences: false
)
let intPart = parts.first.map(String.init) ?? ""
let fractionalPart = parts.count == 2 ? String(decimalSeparator) + parts[1] : ""
// Formatters: one for grouped int display, one for parsing ungrouped ints
return groupedNumber(
intPart: intPart,
fractionalPart: fractionalPart,
locale: locale
)
}
private func groupedNumber(
intPart: String,
fractionalPart frac: String,
locale: Locale
) -> String {
let group = NumberFormatter()
group.locale = locale
group.numberStyle = .decimal
group.usesGroupingSeparator = true
let parseInt = NumberFormatter()
parseInt.locale = locale
parseInt.numberStyle = .decimal
parseInt.usesGroupingSeparator = false
// 3) Group integer part using locale rules
let groupedInt = parseInt.number(from: intPart).flatMap {
group.string(from: $0)
} ?? intPart
return groupedInt + frac
}
}
struct ContentView: View {
@State private var text = ""
@State private var amount = ""
var body: some View {
VStack(alignment: .leading) {
Text("Amount you want to transfer:")
DecimalTextField(title: "Enter amount", text: $text)
Button {
amount = calculateAmount()
} label: {
Text("Send")
}
.frame(maxWidth: .infinity, alignment: .center)
.padding()
Text("Region: \(region)")
Text(amount.isEmpty ? "" : "Total: \(amount)")
}
.padding(20)
}
private func calculateAmount() -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
guard let nsNumber = formatter.number(from: text) else {
return "Error"
}
return "$\(formatter.string(from: nsNumber) ?? "Error")"
}
private var region: String {
Locale.current.localizedString(forRegionCode: Locale.current.region?.identifier ?? "Unknown") ?? "Unknown"
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment