1

I've tried to implement Currency TextField to my project, which I found on this website - https://benoitpasquier.com/currency-textfield-in-swiftui.

It almost does work as expected, but with one major issue: passed value is not showing up in TextField. (eg. Binding = 1000 is still presented as $0.00 in TextField)

Code below:

import Foundation
import SwiftUI
import UIKit

class CurrencyUITextField: UITextField {
    @Binding private var value: Int
    private let formatter: NumberFormatterProtocol

    init(formatter: NumberFormatterProtocol, value: Binding<Int>) {
        self.formatter = formatter
        self._value = value
        super.init(frame: .zero)
        setupViews()
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func willMove(toSuperview newSuperview: UIView?) {
        super.willMove(toSuperview: superview)
        addTarget(self, action: #selector(editingChanged), for: .editingChanged)
        addTarget(self, action: #selector(resetSelection), for: .allTouchEvents)
        keyboardType = .numberPad
        textAlignment = .right
        sendActions(for: .editingChanged)
    }

    override func removeFromSuperview() {
        Log.info("Removed CurrencyTextField from View")
    }

    override func deleteBackward() {
        text = textValue.digits.dropLast().string
        sendActions(for: .editingChanged)
    }

    private func setupViews() {
        tintColor = .clear
        font = .systemFont(ofSize: 40, weight: .regular)
    }

    @objc private func editingChanged() {
        text = currency(from: decimal)
        resetSelection()
        updateValue()
    }

    @objc private func resetSelection() {
        selectedTextRange = textRange(from: endOfDocument, to: endOfDocument)
    }

    private func updateValue() {
        DispatchQueue.main.async { [weak self] in
            self?.value = self?.intValue ?? 0
        }
    }

    private var textValue: String {
        return text ?? ""
    }

    private var decimal: Decimal {
        return textValue.decimal / pow(10, formatter.maximumFractionDigits)
    }

    private var intValue: Int {
        return NSDecimalNumber(decimal: decimal * 100).intValue
    }

    private func currency(from decimal: Decimal) -> String {
        return formatter.string(for: decimal) ?? ""
    }
}

extension StringProtocol where Self: RangeReplaceableCollection {
    var digits: Self { filter(\.isWholeNumber) }
}

extension String {
    var decimal: Decimal { Decimal(string: digits) ?? 0 }
}

extension LosslessStringConvertible {
    var string: String { .init(self) }
}
import SwiftUI
import UIKit

struct CurrencyTextField: View {
    @Binding var value: Int
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 15)
                .foregroundColor(.quaternary)
                .frame(maxHeight: 80)
            CurrencyTextField_(numberFormatter: NumberFormatter.currencyFormatter, value: $value)
                .padding(.horizontal)
        }
        .frame(maxHeight: 80)
    }
}

struct CurrencyTextField_: UIViewRepresentable {
    typealias UIViewType = CurrencyUITextField

    let numberFormatter: NumberFormatterProtocol
    let currencyField: CurrencyUITextField

    init(numberFormatter: NumberFormatterProtocol, value: Binding<Int>) {
        self.numberFormatter = numberFormatter
        currencyField = CurrencyUITextField(formatter: numberFormatter, value: value)
    }

    func makeUIView(context: Context) -> CurrencyUITextField {
        return currencyField
    }

    func updateUIView(_ uiView: CurrencyUITextField, context: Context) {}
}

struct CurrencyTextField_Previews: PreviewProvider {
    static var previews: some View {
        CurrencyTextField(value: .constant(1025))
    }
}

This view can be initialised with amount = 0, or as an 'Edit' view, with injected amount value (eg. 1000, previously created by the same CurrencyTextField)

struct CreateTransactionView: View {
    @State var amount = 0

    var body: some View {
            VStack(alignment: .leading, spacing: 15) {
                CurrencyTextField(value: $amount)
            }
    }
}

What could be possibly causing this issue? PS. I have little to no experience with UIKit.

daniel
  • 38
  • 5

2 Answers2

0

You can use currency in TextField by changing the format which is only available for iOS 15+.

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
extension FormatStyle {
    public static func currency<Value>(code: String) -> Self where Self == FloatingPointFormatStyle<Value>.Currency, Value : BinaryFloatingPoint
}
struct ContentView: View {
    @State private var myMoney: Double? = 300.0
    var body: some View {
        TextField(
            "Currency (USD)",
            value: $myMoney,
            format: .currency(code: "USD")
        )
    }
}

or using Locale

struct ContentView: View {
    private let locale = Locale.current
    @State private var myMoney: Double? = 1000.0
    var body: some View {
        TextField(
            "Currency (\(locale.currencyCode ?? "USD")",
            value: $myMoney,
            format: .currency(code: locale.currencyCode ?? "USD")
        )
    }
}
cole
  • 1,039
  • 1
  • 8
  • 35
-1

updateUIView hasn't been implemented. And makeUIView isn't done correctly.

makeUIView should init the UIView locally and return it.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • Why is this the accepted answer? In our case, we do have these methods implemented, and still having difficulty getting this to work. Can you please elaborate on what you mean by "isn't done correctly"? – quickthyme May 31 '22 at 22:21
  • Ok I elaborated – malhal Sep 18 '22 at 07:39