I'm attempting to set a textfield as first responder using its tag property and a @Binding. As I am unable to access the underlying UITextField from a SwiftUI TextField and call .becomeFirstResponder()
directly, I'm having to wrap a UITextField using UIViewRepresentable
. The code below works but results in the following console message === AttributeGraph: cycle detected through attribute <#> ===
.
It sounds like I have a memory leak and/or retain cycle, I've isolated the issue to the line textField.becomeFirstResponder()
but having inspected Xcode's Memory Graph Hierarchy I can not see what is wrong?
Any help provided is be much appreciated.
struct CustomTextField: UIViewRepresentable {
var tag: Int
@Binding var selectedTag: Int
@Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: ResponderTextField
init(_ textField: ResponderTextField) {
self.parent = textField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.tag = tag
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ textField: UITextField, context: Context) {
if textField.tag == selectedTag, textField.window != nil, textField.isFirstResponder == false {
textField.becomeFirstResponder()
}
}
}
on a hunch, I put becomeFirstResponder() in an async dispatch. This fixes the warnings. I'm guessing there is some kind of creation loop which happens when the view is created, and you call becomeFirstResponder(), so that calls the view, which triggers SwiftUI to find the view which it hasn't properly created yet (or something like that)
anyway - this works for me:
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
//This triggers attribute cycle if not dispatched
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
Confused Vorlon's answer worked for me but it was still a little wonky as it seems that uiView.isFirstResponder
was not returning what I was expecting during Navigating to another view. It resulted in uiView.becomeFirstResponder()
being called an unexpected amount of time.
Another way to fix this, is to add a boolean in your coordinator to track that you've already made that field first responder.
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FocusableTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
class Coordinator: NSObject {
@Binding var text: String
var didBecomeFirstResponder: Bool = false
init(text: Binding<String>) {
_text = text
}
@objc public func textViewDidChange(_ textField: UITextField) {
self.text = textField.text ?? ""
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With