19

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()
        }
    }
}
Elliott
  • 301
  • 1
  • 2
  • 5
  • did you find a solution to this yet? I am encountering the same issue? – Learn2Code Mar 16 '20 at 02:30
  • I can't offer a solution but I have encountered the same problem. The "AttributeGraph: cycle detected ..." message appears several times on the console and under some circumstances the app crashes pointing at textField.becomeFirstResponder(). The crash even happens if I check textField.canBecomeFirstResponder before calling becomeFirstResponder()... – domi852 Apr 14 '20 at 09:38
  • try the following: change your struct CustomTextField into final class (the final attribute is important). then you need to add an explicit initialiser (init(....)). in my case, this seems to solve the AttributeGraph problem. by the way, this is also the solution to the view not being updated when a published attribute of an observed object (passed into this custom text field) changes... – domi852 Apr 15 '20 at 06:27
  • Works fine with Xcode 12 / iOS 14 – Asperi Jul 21 '20 at 15:48
  • I think the retain cycle may be caused by capturing self in the coordinator. I have found a similar cycle I asked about here: https://stackoverflow.com/questions/66540207 – wshamp Mar 09 '21 at 15:00

4 Answers4

41

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
  • 9,659
  • 3
  • 46
  • 49
3

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 ?? ""
    }
}
streem
  • 9,044
  • 5
  • 30
  • 41
0

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.

scaly
  • 509
  • 8
  • 18
0

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

.focusedValue(\.path, $binding)
chengpan
  • 11
  • 1