Is it possible to dynamically update the currency formatting of SwiftUI TextField?

I'd like to be able to dynamically update the format of a SwiftUI TextField based on a user's selection from a Picker, but what I've tried doesn't seem to be working.

I have a TextField where the user enters an amount, and a Picker from which the user selects a currency, and I'd like the formatting of the TextField to match the Picker's currency selection.

TextField has a format property (at least in macOS 12/iOS 15) which allows it to be formatted as a specified currency. That mostly works as expected for whatever currency I initially pass to it, but if I change the currency after the View loads the formatting doesn't change. That is despite passing it the same variable that I pass to the Picker's selected argument, and despite the fact that the View regularly updates when the TextField's value changes.

Below is a simplified version of my code to illustrate the issue. The TextField's value will initially be formatted to show a localised representation of GBP £0.00. If you type “100” in the field it will update to format it as British pounds. All good. But if you change the currency from the Picker to, say, USD, or any other currency, the amount will continue to be formatted as GBP.

For the avoidance of any doubt, I know from my actual code (though it can't be seen in this example) that the value of currency (that is being passed to the Picker and the TextField's format property) is changing based on the Picker selection. So that's not the issue. It's just that the TextField doesn't seem to be aware or care that it has changed.

So it seems like whatever is passed to the TextField's format parameter on initialisation sticks forever and cannot be changed. But if I'm doing something wrong or there's a workaround I'd be really grateful for any pointers.

PS: The issue is best illustrated on macOS, but iOS has the same issue only the formatting appears to change as expected when you first change the selected currency in the Picker, but then reverts once you edit the amount field.

PPS: There's a separate issue relating to formatting EUR which I'll post about separately as it seems to be a separate issue.

import SwiftUI



struct ContentView: View {

	@State private var amount = Decimal()

	@State private var currency: Currency = .GBP



    var body: some View {

CurrencyAmount(

	title: "Some label",

	amount: $amount,

	currency: $currency)

    }

}



struct CurrencyAmount: View {

	let title: String

	@Binding var amount: Decimal

	@Binding var currency: Currency

	let prompt: String = ""



	var body: some View {

		HStack {

		TextField(

			title,

			value: $amount,

			//FIXME: The currency code used in the format does not update when the user selects a different currency from the Picker.

			format: .currency(code: currency.rawValue),

			prompt: Text(prompt))



				CurrencyPicker(selected: $currency)

		}

	} 

}



struct CurrencyPicker: View {

	@Binding var selected: Currency

	var label = "Currency"

	var body: some View {

		Picker(selection: $selected,

			   label: Text(label)

		) {

			ForEach(Currency.allCases) { code in

				Text(code.rawValue).tag(code)

			}

		}

	}

}



enum Currency: String, CaseIterable, Identifiable {

	case AUD, CAD, EUR, GBP, NZD, USD

	var id: String { self.rawValue }

}

I posted about the related issue that specifically relates to the formatting of Euros (EUR) here.

I think this works. Try on simulator. Preview gives unpredictable results.

import SwiftUI

struct Amount {
    var value: Decimal
    var currency: CurrencyCode
}

struct ContentView: View {
    @State var amount = Amount(value: Decimal(), currency: .GBP)

    var body: some View {
        CurrencyAmount(title: "Some label", amount: $amount)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(amount: Amount(value: Decimal(), currency: .GBP))
    }
}

struct CurrencyAmount: View {
    let title: String
    let prompt: String = ""
    @Binding var amount: Amount
    @State var currency: CurrencyCode = .GBP

    var body: some View {
        HStack {
            Spacer(minLength: 80)
            TextField(title, value: $amount.value, format: .currency(code: amount.currency.rawValue), prompt: Text(prompt))
            CurrencyPicker(selected: $currency)
            Spacer(minLength: 80)
        }
        .onChange(of: currency) { newValue in
            amount.currency =  newValue
        }
    }
}

struct CurrencyPicker: View {
    @Binding var selected: CurrencyCode
    let label = "Currency"
    var body: some View {
        Picker(label, selection: $selected) {
            ForEach(CurrencyCode.allCases) {
                Text($0.rawValue)
                    .tag($0)
            }
        }
    }
}

enum CurrencyCode: String, CaseIterable, Identifiable {
    var id: String { self.rawValue }
    case AUD, CAD, EUR, GBP, NZD, USD
}

I understand that you want to change the currency symbol, but also want to change the TextField to show the new currency in the proper format.

I had the same problem. While working on it I realized that currency symbol and number formatting are actually different things, and not necessarily connected. If an American speaks about ten thousand dollars, he will write "$10,000.00". If a German speaks about a thousand Euros, he will write "10.000,00 €". And if he speaks about a thousand dollars, in his own country, he will probably write "10.000,00 $". A Frenchman will write a thousand Euros as "10 000,00 €". A Dutchman will write "€ 10.000,00". Etc.

So, to properly format a currency you need to know both the valuta and the region where the expression is used.

This information is in SwiftUI captured in NumberFormatter.locale, which contains both a language code and a country code. Examples in String form: "en-US", "en-GB", etc. The following code takes care of that.

A trick is used to update the TextField whenever another locale is chosen in the Picker. See the function setLocale(:). This is because the TextField does not monitor the .locale property of the NumberFormatter class object, and I know of no way to make it do so. Therefore amount, which is monitored, is changed and then changed back. An ugly hack, but I think safe and it works (at least until the compiler optimizes it away... and there are ways around that I suppose.) Anyway, I would like to learn of a more Swifty way.

I hope this is what you are looking for!

import SwiftUI

struct ContentView: View {

    private var numberFormatter: NumberFormatter

    init(numberFormatter: NumberFormatter = NumberFormatter()) {
        self.numberFormatter = numberFormatter
        self.numberFormatter.usesGroupingSeparator = true
        self.numberFormatter.numberStyle = .currency
        self.numberFormatter.locale = Locale(identifier: "nl-NL")
    }

    @State private var amount = 1234567.89

    let locales = ["nl-NL", "de-DE", "fr-FR", "en-US", "en-GB", "th-TH", "az-AZ"]

    @State private var locale = "nl-NL"
    @State private var newLocale = "nl-NL"

    func setLocale(to locale: String) {
        numberFormatter.locale = Locale(identifier: locale)
        amount += 1
        amount -= 1
    }

    var body: some View {

        NavigationView {

            Form {
                Text("Current locale: \(locale)")
                TextField("Amount", value: $amount, formatter: numberFormatter)
                Picker("Choose your new locale:", selection: $newLocale) {
                    ForEach(locales, id: \.self) { locale in
                        Text(locale)
                    }
                }
                .onChange(of: newLocale) { newValue in
                    setLocale(to: newLocale)
                    locale = newLocale
                }
            }
        }
    }
}

Disclaimer: Only tested on the XCode simulator.

Is it possible to dynamically update the currency formatting of SwiftUI TextField?
 
 
Q