1

If I create a SwiftUI TextField with the following:

init<S, T>(_ title: S, value: Binding<T>, formatter: Formatter)

it only updates the bound value when Return is pressed. If I select a different field before pressing return the first field still displays its new value, but the bound variable is not updated.

The following init seems like onEditingChanged might help me,

init<S, T>(_ title: S, value: Binding<T>, formatter: Formatter, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {})

But the documentation is practically empty at this time. This earlier question seems similar, but has no published answers and might predate the second init above.

If the bound value is not updated, in the OnCommit action how do I grab the string displayed in the TextField to format it/convert it to a number?

adamek
  • 2,324
  • 3
  • 24
  • 38

2 Answers2

2

try something like this:

struct ContentView: View {

@State var test1 = "test1"
@State var test2 = "test2"

var body: some View {
    VStack {
        TextField(test1, text: Binding(get: {
            self.test1
        }, set: { newVal in
            self.test1 = newVal
            self.test2 = newVal
        }))
        TextField(test2, text: $test2)
    }
}
  • This does not work for the constructor with `formatter`, you'd read question carefully. – Asperi Apr 09 '20 at 14:05
  • 1
    Correct, it does not work with the formatter, as I understand this is a bug. However the question "How to get bound value of a TextField to update when you tap another TextField?" is answered using another approach. – workingdog support Ukraine Apr 10 '20 at 00:01
0

Thank you to workingdog for what was almost the answer. It did not work well with formatter, but it gave me the idea that did work.

Formatting right inside the binding doesn't work because the cursor sometimes jumps around after formatting.

First my cash formatter:

let cashFormat = getCashFormat()
func getCashFormat() -> NumberFormatter {
    let thisCashFormat = NumberFormatter()
    thisCashFormat.numberStyle = NumberFormatter.Style.currency
    thisCashFormat.roundingMode = NumberFormatter.RoundingMode.halfEven
    // halfEven rounding is sometimes referred to as Bankers’ rounding.
    thisCashFormat.maximumFractionDigits = 2
    return thisCashFormat
}

cashFormat formatting will return a nil if the string doesn't have a $ as the first character. I need another NumberFormatter.

let basicFormat = getBasicFormat()
func getBasicFormat() -> NumberFormatter {
    let thisBasicFormat = NumberFormatter()
    thisBasicFormat.numberStyle = NumberFormatter.Style.decimal
    thisBasicFormat.roundingMode = NumberFormatter.RoundingMode.halfEven
    thisBasicFormat.minimumFractionDigits = 0
    thisBasicFormat.maximumFractionDigits = 6
    return thisBasicFormat
}

basicFormat returns more fraction digits than I need, but I'll be converting the double into integer cents elsewhere in my app before the model stores it.

I created an ObservableObject class for the formatting.

class FieldFormatter: ObservableObject {
    @Binding var dollars: Double
    @Published var dollarText: String

    init(dollars: Binding<Double>) {
        self._dollars = dollars
        self.dollarText = dollars.wrappedValue.cash()
    }

    func dollarsChanged() {
        if let thisNumber = cashFormat.number(from: dollarText) {
            self.dollars = thisNumber.doubleValue
        } else if let thisNumber = basicFormat.number(from: dollarText) {
            self.dollars = thisNumber.doubleValue
        }
    }
}

Finally, here's my TextField struct.

struct CashFieldC: View {

    var thisLabel: String
    @Binding var dollars: Double
    @ObservedObject var fieldFormatter: FieldFormatter

    init(thisLabel: String, dollars: Binding<Double>) {
        self.thisLabel = thisLabel
        self._dollars = dollars
        self.fieldFormatter = FieldFormatter(dollars: dollars)
    }

    var body: some View {
        VStack(alignment: .leading) {
            Text(thisLabel).font(.caption)
            TextField(thisLabel, text: $fieldFormatter.dollarText, onEditingChanged: { (oec) in
                if !oec {
                    self.fieldFormatter.dollarsChanged()
                }
            })
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .keyboardType(.numbersAndPunctuation)
        }
    }
}

The dollarText value is set when FieldFormatter object is initialized. The binding in the TextField updates it every time a character is entered.

The bound variable in a formatting field only gets updated when the field is committed on hitting return.

Formatting inside a Binding close to workingdog's answer would cause the cursor to jump around after entering each digit.

This struct uses the onEditingChanged: closure to update the dollars binding when the field looses focus from hitting return or when selecting another field. The closure will attempt a cash formatting first or a basic double formatting if the cash formatting fails.

adamek
  • 2,324
  • 3
  • 24
  • 38
  • 1
    This is helpful but what's the implementation behind dollars.wrappedValue.cash() in FieldFormatter init()? – John Hurrell Apr 30 '20 at 14:08
  • Extension to Double func cash() -> String { if let cashString = cashFormat.string(from: NSNumber(value: self)) { return cashString } else { return "$$$curious cash extension bug$$$" } } – adamek Apr 30 '20 at 14:12