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 }

}
Add a Comment

Replies

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
}
  • Thanks, @anils, that got me part of the way there. When I run your solution on both macOS and iOS (not in the simulator, but actually running it on my devices in both cases), the TextField shows “GBP 0.00” on launch. If I delete the contents of the TextField and type “100” it resolves to “GBP 100.00”. So far so good. When I change the currency in the CurrencyPicker to e.g. NZD the TextField changes to display “NZD 100.00”. Fantastic – at this point I thought the issue had been solved. But then if I type anything else in the TextField, e.g. if I delete what's there and type “200”, the formatting reverts to GBP e.g. “GBP 200.00” (although the underlying value for currency and amount.currency and the value displayed in the Picker will all remain .NZD).

    Funnily enough, although I initially thought this was at least some progress, when I went back to my original code I realised that my original code did the same thing (i.e. the formatting would initially update when the Picker value was first changed, but would revert as soon as something else is typed in the TextField).

    Interestingly, I did notice a slight difference between macOS and iOS. In your solution, the CurrencyAmount View takes a binding to an Amount struct which has value and currency properties. It also has a separate @State variable for currency which it passes to the CurrencyPicker instead of $amount.currency. And then it uses the .onChange modifier to keep currency and amount.currency in sync.

    On macOS, I can simplify your solution, delete the @State variable for currency, and just pass $amount.currency to the Currency Picker and get the exact same effect (example code below). But on iOS, the below code results in the Picker continuing to display the initial selection even when the underlying value is changed. So your additional step seems necessary on iOS but not on macOS.

    For what it's worth, here's my code that had the same effect as @anils' suggestion on macOS but not on iOS.

    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 { NewCurrencyAmount(title: "Some label", amount: $amount) } } struct NewCurrencyAmount: View { let title: String let prompt: String = "" @Binding var amount: Amount var body: some View { HStack { Spacer(minLength: 80) TextField(title, value: $amount.value, format: .currency(code: amount.currency.rawValue), prompt: Text(prompt)) NewCurrencyPicker(selected: $amount.currency) Spacer(minLength: 80) } } } struct NewCurrencyPicker: 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 }
Add a Comment

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.