I wanted to preform live formatting to a value entered into a SwiftUI TextField, so I used introspect to add a custom delegate, like in UIKit. This caused the .onChange to stop working. If I comment out the delegate, the .onChange block works. How do I get the binding to work with the custom delegate?
TextFieldView:
import Foundation
import SwiftUI
struct TextFieldView: View {
@State private var inputString: String = ""
var customDelegate = CustomTextFieldDelegate()
var body: some View {
VStack {
TextField("", text: $inputString)
.addTextFieldDelegate(delegate: customDelegate)
}
.onChange(of: inputString) { _ in
print("inputString: \(inputString)")
}
}
}
Introspect:
extension View {
func addTextFieldDelegate(delegate: UITextFieldDelegate) -> some View {
introspect(.textField, on: .iOS(.v15...)) { textfield in
textfield.delegate = delegate
}
}
}
Custom Delegate:
class CustomTextFieldDelegate: NSObject, UITextFieldDelegate {
override init() { }
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text, let textRange = Range(range, in: text) else {
return false
}
var updatedText = text.replacingCharacters(in: textRange, with: string)
updatedText.removeAll(where: {$0 == ":"})
let finalLength = updatedText.count + updatedText.count/2 - 1
if finalLength > 8 {
return false
}
for i in stride(from: 2, to: finalLength, by: 3) {
let index = updatedText.index(updatedText.startIndex, offsetBy: i)
updatedText.insert(":", at: index)
}
textField.text = updatedText
return false
}
}
Edit 01:
What I want to achieve is to add a ":" ever two characters as the input is being typed into the TextField. I would prefer not to use introspect, but not sure how to modify the variable inputString without triggering the onChange handler again, thus leading to an infinite loop.
You should not rely on the fact that SwiftUI is built on UIKit - This could well change in the future.
You don't need to use the UITextFieldDelegate
- You can perform your processing in the onChange
handler.
We can check that the old and new values are the same to prevent an onChange
loop.
For example:
struct ContentView: View {
@State var oldValue = ""
@State var inputString = ""
var body: some View {
VStack {
TextField("XX:XX:XX:XX", text:$inputString)
}
.onChange(of: inputString) { newValue in
self.formatTextField(newValue)
}
}
func formatTextField(_ newValue: String) {
guard self.oldValue != self.code else {
return
}
var finalValue = newValue
if newValue.count > self.oldValue.count {
finalValue = self.processInsert(newValue: newValue, oldValue: self.oldValue)
} else {
finalValue = self.processDelete( newValue: newValue, oldValue: self.oldValue)
}
self.oldValue = finalValue
self.inputString = finalValue
}
func processInsert(newValue: String, oldValue:String) -> String {
guard newValue.count < 12 else {
return oldValue
}
var characters = newValue
characters.removeAll { $0 == ":"}
var finalValue = ""
for i in 0..<characters.count {
finalValue.append(characters[characters.index(characters.startIndex,offsetBy: i)])
if i < 7 && i > 0 && (i-1) % 2 == 0 {
finalValue.append(":")
}
}
return finalValue
}
func processDelete(newValue: String, oldValue: String) -> String {
var finalValue = newValue
var input = finalValue
input.removeAll { $0 == ":"}
let length = input.count
if length > 0 && length % 2 == 0 {
finalValue = String(finalValue.prefix(finalValue.count-1))
}
return finalValue
}
}