Last active
March 24, 2022 14:47
-
-
Save damodarnamala/97b25d78ffbce090d4458c506fa7d32d to your computer and use it in GitHub Desktop.
SwiftUI Custom textfield for placeholder and manager currency
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// Amount | |
// | |
// Created by Damodar Namala on 25/02/22. | |
// | |
import SwiftUI | |
import Combine | |
struct ContentView: View { | |
@State private var price: Double? | |
@State var error: AmountError = .none | |
@State var amountError: AmountError? = .invalidAmount | |
@State var currency: Currencies = .DOP | |
@State var shouldEnabled: Bool = false | |
var body: some View { | |
ZStack { | |
VStack { | |
AmountField(amount: $price, | |
currency: $currency, | |
error: $amountError) | |
AmountField(amount: $price, | |
currency: $currency, | |
error: $amountError, | |
shouldDisable: .constant(true)) | |
Button("Print") { | |
UIApplication.shared.endEditing() | |
if let price = price { | |
print(price) | |
} | |
} | |
Button("DOP 500") { | |
currency = .DOP | |
UIApplication.shared.endEditing() | |
price = 50066 | |
} | |
Button("USD 400") { | |
currency = .USD | |
UIApplication.shared.endEditing() | |
price = 400663 | |
} | |
} | |
} | |
.onTapGesture { | |
UIApplication.shared.endEditing() | |
} | |
} | |
} | |
struct AmountField: View { | |
@Binding var amount: Double? | |
@Binding var currency: Currencies | |
@Binding var error: AmountError? | |
@Binding var shouldDisable: Bool | |
init(amount: Binding<Double?>, | |
currency: Binding<Currencies>, | |
error: Binding<AmountError?>, | |
shouldDisable: Binding<Bool> = .constant(false)) { | |
self._amount = amount | |
self._currency = currency | |
self._error = error | |
self._shouldDisable = shouldDisable | |
} | |
var body: some View { | |
VStack(alignment: .leading, spacing: 4) { | |
Text("Amount") | |
.font(.headline) | |
.frame( maxWidth: .infinity, alignment: .leading) | |
HStack { | |
DecimalTextField(currency: $currency, | |
value: $amount, | |
formatter: formatter(value: "\(amount ?? 0.00)")) | |
{ isEditing in | |
self.error = AmountError.none | |
} | |
.frame(height: 54, alignment: .center) | |
.cornerRadius(8) | |
} | |
Divider() | |
if let error = error, let desc = error.errorDescriptiom { | |
if error != .none && error.errorDescriptiom != nil { | |
Text(desc) | |
.foregroundColor(.red) | |
.font(.footnote) | |
} | |
} | |
} | |
.cardStyle() | |
.disabled(shouldDisable) | |
} | |
} | |
typealias AmountBlock = ((Double) -> Void) | |
enum AmountError { | |
case insufficientBalance | |
case invalidAmount | |
case none | |
var errorDescriptiom: String? { | |
switch self { | |
case .insufficientBalance: | |
return "insufficientBalance" | |
case .invalidAmount: | |
return "Amount must be > 0" | |
case .none: | |
return nil | |
} | |
} | |
} | |
struct DecimalTextField: UIViewRepresentable { | |
@Binding var currency: Currencies | |
@Binding var value: Double? | |
var isEditing: BoolBock? | |
private let currencyLabel = UILabel() | |
private var formatter: NumberFormatter | |
init(currency: Binding<Currencies>, | |
value: Binding<Double?>, | |
formatter: NumberFormatter, | |
isEditing: BoolBock?) { | |
self._value = value | |
self.formatter = formatter | |
self.isEditing = isEditing | |
self._currency = currency | |
} | |
func makeUIView(context: Context) -> TextFieldPadding { | |
let textfield = TextFieldPadding() | |
textfield.delegate = context.coordinator | |
textfield.textAlignment = .left | |
textfield.keyboardType = .decimalPad | |
textfield.setContentHuggingPriority(.defaultHigh, for: .vertical) | |
textfield.setContentHuggingPriority(.defaultLow, for: .horizontal) | |
textfield.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) | |
return textfield | |
} | |
func updateUIView(_ uiView: TextFieldPadding, context: Context) { | |
currencyLabel.text = currency.currencyString | |
uiView.leftViewMode = .always | |
uiView.leftView = currencyLabel | |
uiView.placeholder = currency.placeholder | |
if let val = value { | |
uiView.text = self.formatter.string(for: val) | |
} | |
} | |
func makeCoordinator() -> Coordinator { | |
Coordinator(self, isEditing: isEditing) | |
} | |
class Coordinator: NSObject, UITextFieldDelegate { | |
var parent: DecimalTextField | |
var isEditing: BoolBock? | |
init(_ textField: DecimalTextField, isEditing: BoolBock?) { | |
self.parent = textField | |
self.isEditing = isEditing | |
} | |
func textField(_ textField: UITextField, | |
shouldChangeCharactersIn range: NSRange, | |
replacementString string: String) -> Bool { | |
self.isEditing?(true) | |
return true | |
} | |
func textFieldDidEndEditing(_ textField: UITextField, | |
reason: UITextField.DidEndEditingReason) { | |
textField.text = textField.text?.formatted() | |
self.parent.value = textField.text?.toDouble | |
} | |
} | |
} | |
extension String { | |
private var remveStrings: String { | |
return self.replacingOccurrences(of: ",", with: "", options: .numeric) | |
} | |
var toDouble: Double { | |
return Double(self.remveStrings) ?? 0.00 | |
} | |
func formatted(value: String = "") -> String { | |
let number = NSNumber(value: self.toDouble) | |
let formatter = NumberFormatter() | |
formatter.locale = Locale.usd | |
formatter.numberStyle = .decimal | |
formatter.maximumFractionDigits = 2 | |
formatter.minimumFractionDigits = (value.contains(".00")) ? 0 : 2 | |
guard let res = formatter.string(from: number) else {return self} | |
return "\(res)" | |
} | |
} | |
func formatter(value: String) -> NumberFormatter { | |
let formatter = NumberFormatter() | |
formatter.minimumFractionDigits = (value.contains(".00")) ? 0 : 2 | |
formatter.maximumFractionDigits = 2 | |
formatter.groupingSize = 3 | |
formatter.secondaryGroupingSize = 2 | |
formatter.numberStyle = .decimal | |
return formatter | |
} | |
typealias BoolBock = ((Bool) -> Void) | |
class TextFieldPadding: UITextField { | |
let padding = UIEdgeInsets(top: 0, left: 54, bottom: 0, right: 8) | |
override open func textRect(forBounds bounds: CGRect) -> CGRect { | |
return bounds.inset(by: padding) | |
} | |
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect { | |
return bounds.inset(by: padding) | |
} | |
override open func editingRect(forBounds bounds: CGRect) -> CGRect { | |
return bounds.inset(by: padding) | |
} | |
} | |
extension Locale { | |
static let usd = Locale(identifier: "en_US") | |
} | |
/// No need this code | |
enum Currencies: String, CaseIterable, Identifiable { | |
var id: UUID { return UUID() } | |
case USD | |
case DOP | |
case RD | |
var code: String { | |
switch self { | |
case .DOP: | |
return "DOP" | |
case .RD: | |
return "DOP" | |
case .USD: | |
return "USD" | |
} | |
} | |
var placeholder: String { | |
return "0.00" | |
} | |
var currencyString: String { | |
switch self { | |
case .USD: | |
return "USD$" | |
case .DOP: | |
return "DOP$" | |
case .RD: | |
return "DOP$" | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment