I have SwiftUI View where are two normal TextFields and one UIViewRepresentable UITextField.
I need to turn off possibility of copy/paste/select on UIViewRepresentable while typing values.
I'm doing that by set the false value to isUserInteractionEnabled under textFieldShouldBeginEditing function called on coordinator of my CustomUITextField struct (that UIViewRepresentable UITextField).
The problem is that if I press return on some SwiftUI TextFields than textFieldShouldBeginEditing is triggering and setting my CustomUITextField to non-editable before it will be active and therefore the CustomUITextField became untouchable so I can't type any values.
This the MRE
import SwiftUI
struct ContentView: View {
@State private var name: String = ""
@State private var phone: String = ""
@State private var isFirstResponder = false
var body: some View {
VStack {
Spacer()
HStack {
Text("A")
TextField("", text: $name)
.border(Color.gray.opacity(0.3))
}
HStack {
Text("B")
CustomUITextField(isFirstResponder: $isFirstResponder, text: $phone)
.frame(height: 22)
.border(Color.gray.opacity(0.3))
}
Spacer()
}
.padding()
}
}
#Preview {
ContentView()
}
struct CustomUITextField: UIViewRepresentable {
@Binding var isFirstResponder: Bool
@Binding var text: String
init(isFirstResponder: Binding<Bool>, text: Binding<String>) {
self._isFirstResponder = isFirstResponder
self._text = text
}
func makeUIView(context: UIViewRepresentableContext<CustomUITextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.text = text
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomUITextField>) {
context.coordinator.listenToChanges = false
if isFirstResponder != uiView.isFirstResponder {
if isFirstResponder {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
uiView.text = text
context.coordinator.listenToChanges = true
}
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator(isFirstResponder: $isFirstResponder, text: $text)
return coordinator
}
class Coordinator: NSObject, UITextFieldDelegate {
@Binding var isFirstResponder: Bool
@Binding var text: String
fileprivate var listenToChanges: Bool = false
init(isFirstResponder: Binding<Bool>, text: Binding<String>) {
self._isFirstResponder = isFirstResponder
self._text = text
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
textField.isUserInteractionEnabled = false
return true
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
textField.isUserInteractionEnabled = true
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
guard listenToChanges else { return }
isFirstResponder = textField.isFirstResponder
}
func textFieldDidEndEditing(_ textField: UITextField) {
guard listenToChanges else { return }
isFirstResponder = textField.isFirstResponder
text = textField.text ?? ""
}
}
}
If you enter some value on TextField A and press return that you can't activate TextField B.
I was tried pass boolean value through binding to UIViewRepresentable coordinator while I'm tapping on TextField A to make some if statement in textFieldShouldBeginEditing but that function is triggering before .onSubmit event on A field.
Any thoughts about workaround of this problem?
Thanks in advance
Instead of using textFieldShouldBeginEditing
like this, you should do something similar to this answer, i.e. override canPerformAction
to return false.
struct CustomUITextField: UIViewRepresentable {
@Binding var text: String
class Wrapper: UITextField {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
false
}
// if you want to disable the magnifying glass and the "autofill" popup too,
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
!(gestureRecognizer is UILongPressGestureRecognizer)
}
override func buildMenu(with builder: any UIMenuBuilder) {
builder.remove(menu: .autoFill)
super.buildMenu(with: builder)
}
}
init(text: Binding<String>) {
self._text = text
}
func makeUIView(context: UIViewRepresentableContext<CustomUITextField>) -> UITextField {
let textField = Wrapper(frame: .zero)
textField.textContentType = UITextContentType(rawValue: "")
textField.text = text
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomUITextField>) {
context.coordinator.textCallback = { text = $0 }
uiView.text = text
}
func makeCoordinator() -> Coordinator {
.init()
}
class Coordinator: NSObject, UITextFieldDelegate {
var textCallback: ((String) -> Void)?
func textFieldDidEndEditing(_ textField: UITextField) {
textCallback?(textField.text ?? "")
}
}
}
You never know when textFieldShouldBeginEditing
is called, so it is generally a bad idea to put any side effects in there.