Reputation: 19
I want to write a custom TextField on SwiftUI to have an input with credit card format like this "xxxx-xxxx-xxxx-xxxx", I was looking at this answers https://stackoverflow.com/a/48252437, but I'm struggling with UIViewRepresentable, when I want to past the numbers into the original text, it doesn't seems to update it with the format.
The textField will show you 12345, but I need 1234 5
struct ContentView: View {
@State var cardNumber: String = "12345" // <-- It won't be formatted until I type another one number
var body: some View {
VStack {
CreditCardTextField(number: $cardNumber)
.frame(height: 50)
.border(.black)
}
.padding()
}
}
And UIViewRepresentable where I transported the code from the link answer
struct CreditCardTextField: UIViewRepresentable {
@Binding public var number: String
public init(number: Binding<String>) {
self._number = number
}
public func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.addTarget(context.coordinator, action: #selector(Coordinator.reformatAsCardNumber), for: .editingChanged)
view.delegate = context.coordinator
return view
}
public func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = number // <-- I believe in here I should update the code so that it will be formatted, but I can't get how to refactore the code
}
public func makeCoordinator() -> Coordinator {
Coordinator($number)
}
// MARK: Coordinator
public class Coordinator: NSObject, UITextFieldDelegate {
@Binding var number: String
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?
init(_ number: Binding<String>) {
self._number = number
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
previousTextFieldContent = textField.text
previousSelection = textField.selectedTextRange
return true
}
@objc func reformatAsCardNumber(textField: UITextField, for event: UIControl.Event) {
var targetCursorPosition = 0
if let startPosition = textField.selectedTextRange?.start {
targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
}
var cardNumberWithoutSpaces = ""
if let text = textField.text {
cardNumberWithoutSpaces = self.removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
}
if cardNumberWithoutSpaces.count > 16 {
textField.text = previousTextFieldContent
textField.selectedTextRange = previousSelection
return
}
let cardNumberWithSpaces = self.insertCreditCardSpaces(cardNumberWithoutSpaces, preserveCursorPosition: &targetCursorPosition)
textField.text = cardNumberWithSpaces
number = cardNumberWithSpaces
if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
}
}
func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var digitsOnlyString = ""
let originalCursorPosition = cursorPosition
for i in Swift.stride(from: 0, to: string.count, by: 1) {
let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
if characterToAdd >= "0" && characterToAdd <= "9" {
digitsOnlyString.append(characterToAdd)
} else if i < originalCursorPosition {
cursorPosition -= 1
}
}
return digitsOnlyString
}
func insertCreditCardSpaces(_ string: String, preserveCursorPosition cursorPosition: inout Int) -> String {
var stringWithAddedSpaces = ""
let cursorPositionInSpacelessString = cursorPosition
for i in 0..<string.count {
if i > 0 && (i % 4) == 0 {
stringWithAddedSpaces.append(" ")
if i < cursorPositionInSpacelessString {
cursorPosition += 1
}
}
let characterToAdd = string[
string.index(string.startIndex, offsetBy: i)
]
stringWithAddedSpaces.append(characterToAdd)
}
return stringWithAddedSpaces
}
}
}
Upvotes: 1
Views: 19396
Reputation: 11
just implement Coordinator with parent parameter (your CreditCardTextField) and implement UITextFieldDelegate method textViewDidChange - make all manipulations with string there and apply result to parent.number
Upvotes: 0
Reputation: 30746
Essentially the problem is updateUIView
is missing some logic, e.g. it needs to check if the textField
already contains the formatted number and if it doesn't it needs to format it and set it. Another bug is you don't set the new binding on the coordinator. The binding value changes every time the representable is init and you only gave it the first version when you init the Coordinator
.
Fyi binding is just a pair of get/set closures so it's unusual to pass a binding to the coordinator because it doesn't need the get (also its designed for updating SwiftUI Views not NSObjects). You would be better setting a didChange
closure in updateUIView
. Note sometimes we set it nil before we do our other logic of checking the UIView and setting the new data (because we don't want to get in an update loop).
If I was going to redesign this I would first make a formatting the text a static method that takes a string and returns a string, rather than have that code in the target action. That way it can used from the 2 places it is needed.
Upvotes: -2