AttributeGraph: cycle detected

Hi all!


When presenting a specific modal sheet in a SwiftUI project, I consistently get a ton of console output like:


=== AttributeGraph: cycle detected through attribute 290 ===


Like, a couple hundred of these logs. It appears to be accompanied by a stutter/slowdown in the UI, but there's no crash and everything looks to be in the right place.


Is the "cycle" here some sort of ARC retain cycle? Is it SwiftUI-specific? (I'm asking here because I've never seen this warning in a classic UIKit project.) Where can I go to learn more about this?


Thanks!

  • Is there a way to use the 'attribute number' that appears in the warning to determine what attribute is causing the issue? Otherwise what's the point of providing the attribute number?

Add a Comment

Replies

I tested on OSX 10.14.6, in playground.

struct ContentView: View {
    @State private var showGreeting = false

    var body: some View {

        VStack {
            Button(action: {
                self.showGreeting.toggle()
            }) {
                Text("Toggle Greeting")
            }

            if showGreeting {
                Text("Hello World!")
            }
        }
    }
}

let viewController = UIHostingController(rootView: ContentView())

PlaygroundPage.current.liveView = viewController


I observed the same problem.


It comes form

      if showGreeting {
          // even if this commented out // Text("Hello World!")
     }


Replacing the form by a VStack gets rid of the problem.

In addition, problem seems to appear only in playground "simulator".


Do you use form in your code ?

Could you post the code ?

AttributeGraph is an internal component used by SwiftUI to build a dependency graph for your data and its related views. This is how it determines that when some State value somewhere changes that it needs to update these three views, but not these two sub-views of one of them.


I'd advise looking at the state of your bindings and observed objects, and whether you can simplify how you're using them in any way. For example, you might be passing around an ObservedObject reference rather than individual single-use bindings to its content, and that might result in circular dependencies, for instance:


ContentView:
 - @ObservedObject someThing
 - body:
   - if someThing.someValue {
     - show ModalView(someThing):
       - @ObservedObject someThing
       - someFunc()
         - modify someThing.someValue
       - body:
         - if someThing.someValue
           - possible circular dependency?


I'm sort of reaching towards the thought that both the modal view and the view presenting that modal view are both dependending on the same piece of state somehow, with the modal view determining its content based on that state while the presenter decides whether to show the modal using the same value (or same container ObservedObject perhaps).


You may be able to track this down with the aid of Instruments. Specifically, under SwiftUI there's a ‘View Properties’ instrument that looks at how dynamic view properties are changing over time, and this may provide output that will clearly indicate the presence of a dependency cycle.


Thinking about it, maybe there's a static analysis tool that will find it, too.

Thanks Jim for the overview on AttributeGraph. I'm getting some weird errors trying to build and run for Instruments right now, but I'll definitely look into the SwiftUI profiler to confirm the issue.


In the meantime:

You pointed out the pattern of initializing a subview's @ObservedObject with an object that the parent is also subscribed to.


I am indeed using that pattern in my modal view -- although it doesn't affect the logic around whether the modal is presented.


Is that pattern a misuse of SwiftUI? Or is the problem more like: the framework is still rough around the edges and sometimes has trouble deciding the order in which to evaluate / draw views?


Changing most of my view data to use value types might be a good idea anyway, but I'm a little confused around whether the framework is trying to force me to do this.

I'm not sure if that's a bad pattern to use, but it's the closest I could get to a guess at the sort of thing that might cause a dependency cycle. Generally, your views are a function of your data, so if the creation of a view affects some state data that's used to determine whether that view should appear, or what form it should take, that's likely how a cycle would appear.


It may be something that affects @ObservedObject in particular because I believe it's publishing 'updated' notifications based on the object as a whole, not for individual properties. So, if your object has two properties, and View A uses/modifies property 1 to present View B which uses/modifies property 2, then both are going to depend on the same object, albeit in a way where SwiftUI can't easily determine if there's an order of precedence there. Where possible, try passing bindings down the stack, so the precedence is clear. It's useful to treat @ObservedObject like @State in this respect: there's a single source of truth where that lives, and everything else binds to it, or to the values within it. I don't recall off the top of my head whether '$myObservedObject' would yield a Binding to your observed object itself (i.e. with implied precedence), but '$myObservedObject.someProperty' definitely creates a binding to the underlying value. If you can use that, you'll likely find things easier to reason about.


Another way to think of it is as owner vs. editor. One thing owns state, the other is a helper purely for editing the state. That's the design of the system controls, for example: none of them own any state directly, they just modify something else. View presentation state and suchlike are ultimately owned by ancillary objects created as view modifiers or (internal) generic wrapper views.


In general, SwiftUI would like you to use value types wherever you can. When the project started, that was the whole ethos: using immutable value types for as much as possible, with mutations being a) locked down and minimized, and b) closely monitored to inform regeneration of immutable view descriptions. Views in particular were *only* value types, and the means of updating them was always to re-create the structure itself. That was a conscious decision to prevent bugs related to mutable view state.


For a good overview, WWDC 2016 session 419 can basically be thought of as "lessons learned while writing SwiftUI." It's two engineers on that team describing the ethos behind what would eventually be named SwiftUI.

Same issue here. Using Form as well

Basically, it occurs when you have the first responder present in base view and try to present a sheet.

So before presenting the sheet Dismiss first responder.