Can clear first character when using TextField with NumberFormatter and zeroSymbol set to ""

Hello, I have an issue with formatting text in a TextField. I want to maintain formatting to two decimal places and have it respond to changes in the device's settings, as it currently does. However, I want to get rid of the problem with deleting the first character, which for some reason cannot be empty even though it should start with an empty value.

This is my code

extension Formatter {
    static let numberFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.currencyCode = "GBP"
        formatter.currencySymbol = ""
        formatter.minimumFractionDigits = 2
        formatter.maximumFractionDigits = 2
        formatter.zeroSymbol = ""
        return formatter
    }()
}

struct HomeView: View {
    @FocusState private var isFocused: Bool
    @State var value: Double = 0
    
    var body: some View {
        VStack {
            TextField("0.0",
                      value: $value,
                      formatter: Formatter.numberFormatter)
            .focused($isFocused)
            .border(.red)
            .keyboardType(.decimalPad)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    if isFocused {
                        Spacer()
                        Button("Done") {
                            isFocused = false
                        }
                    }
                }
            }
        }
    }
}

and here is the issue gif

I found similar problems in other forums like in https://www.hackingwithswift.com/forums/swiftui/textfield-with-initial-empty-value-for-a-number/12486/15516 but unfortunately uncomment.

Replies

Hi,

In case SwiftUI decodes the placeholder, it could be that the string "0.0" isn’t recognized by the formatter.
You could try localizing 0.0 so it matches the formatter’s localization (e.g. , instead of . in your GIF → 0,00).
You could also try making the formatter isLenient = true.

Otherwise does it work if you make value an optional?
@State var value: Double?
If that works, you could then post-process nil as 0.

  • In the first case, I tried formatting the placeholder, but it didn't help and results are same.

    Setting isLenient to true also doesn't improve the situation.

    However, using Double? doesn't allow entering text and works only if I refactor my textfield into TextField(format: .number) but In this case, the text inside the TextField doesn't respond to the format change or I do something wrong.

Add a Comment

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
        }
    }
}
  • OK and even with InputValidator works well I see

  • "without"

Add a Comment

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)
    }
}