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()
        }
    }
}
4

There are 4 best solutions below

3
Confused Vorlon On

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()
        }
    }

}
0
scaly On

I was also getting === AttributeGraph: cycle detected through attribute <#> === around a crash, but it turned out the crash was caused by something else.

The crash was actually caused by a missing dependency that was not being linked into the app itself.

Also I checked in instruments, and there were also no memory leaks or retain cycles shown.

The missing dependency was another framework that was supplying some of the SwiftUI view modifiers used in our SwiftUI code. But someone had accidentally removed that framework from the application's "Link Binary With Libraries" and "Embed Frameworks" build phases. Once I added the dependency to the proper build phases and added to be built in the main app scheme, then the log message in question stopped appearing (and the crashes stopped also).

In other words, === AttributeGraph: cycle detected through attribute <#> === does not seem to directly cause crashes or memory leaks, but could be related to some other issue causing (like a missing dependency) SwiftUI to be unable to resolve the view hierarchy.

YMMV.

2
chengpan On

I delete this code in the parent view, warning is disappear,

.focusedValue(\.path, $binding)
0
streem On

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 ?? ""
    }
}