tabViewBottomAccessory AttributeGraph cycles, broken behavior of views participating in cycles

Seemingly innocuous contents passed to tabViewBottomAccessory can trigger inscrutable AttributeGraph cycles, which can then cause unexplained broken behavior of views that may be participating in these cycles.

These cycles can be introduced by adding common elements to the tabViewBottomAccessory view hierarchy, like Slider, Button, Toggle, and even things simple if statements surrounding Text elements. These cycles can even also be triggered in a manner that causes the tabViewBottomAccessoryPlacement Environment value to be nil, which can then cause views that depend on this value to render incorrectly or not at all.

The errors logged to the Xcode console are of the form:

=== AttributeGraph: cycle detected through attribute 29528 ===
=== AttributeGraph: cycle detected through attribute 324264 ===

No further information about this attribute is available in any public Xcode tools.

Environment

  • XCode Version 26.0 (17A324)
  • iOS 26.0 (23A343)

Steps to reproduce

  1. Run the sample above in Simulator
  2. Observe no AttributeGraph cycles in Xcode console.
  3. Uncomment any of the commented out examples in SliderView.body
  4. Observe Xcode console for AttributeGraph cycle messages.
  5. Observe glitchy animation behavior

Expected Behavior

  • No AttributeGraph cycle diagnostics for ordinary state changes.
  • tabViewBottomAccessoryPlacement always present (non-nil) while accessory is attached.
  • Dependent views update consistently.
  • Errors logged to the Console would help guide me towards a resolution

Impact

  • Undermines confidence in adopting tabViewBottomAccessory.
  • Hard to debug: cycle traces are opaque and environment silently degrades (becomes nil) instead of asserting.
  • Nearly shipped a UI where accessory layout fails sporadically.

What would help

  • Underlying fix to prevent cycles for ordinary accessory content mutations.
  • Guarantee (or documented contract) that tabViewBottomAccessoryPlacement is never nil while accessory is active, or an assert if invariants break.
  • Option to enable detailed environment propagation trace when a cycle is detected.
  • Symbolic source identifiers in cycle backtraces.
  • Documentation note on current limitations (if certain view types are not yet supported in accessory regions).
  • Xcode Version 26.0 (17A324)
  • iOS 26.0 (23A343)
  • Affects both devices (iPhone 16 Pro) and simulator (iPhone 17 Pro)

Here's an example that demonstrates how these AttributeGraph: cycle logs are correlated with broken / unexpected behavior of views involved in the cycle. Clicking on the Button to toggle showOptionA doesn't update which "Slider" is displayed:

import SwiftUI

struct ContentView: View {
  var body: some View {
    TabView {
      Tab("Home", systemImage: "house") { DemoView(prefix: "Home") }
      Tab("Alerts", systemImage: "bell") { DemoView(prefix: "Alerts") }
      TabSection("Categories") {
        Tab("Climate", systemImage: "fan") { DemoView(prefix: "Climate") }
        Tab("Lights", systemImage: "lightbulb") { DemoView(prefix: "Lights") }
      }
    }
    .tabViewBottomAccessory { MusicPlaybackView() }
    .tabBarMinimizeBehavior(.onScrollDown)
  }
}

struct DemoView: View {
  let prefix: String
  var body: some View {
    List(0..<50, id: \.self) { i in
      Text("\(prefix) Item #\(i + 1)")
    }
  }
}

struct MusicPlaybackView: View {
  @Environment(\.tabViewBottomAccessoryPlacement) var placement
  var body: some View {
    if placement == .inline { ControlsPlaybackView() } else { SliderPlaybackView() }
  }
}

struct ControlsPlaybackView: View {
  var body: some View { Text("Controls") }
}

struct SliderPlaybackView: View {
  @State private var showOptionA = false
  var body: some View {
    HStack {
      if showOptionA { Text("Slider A") } else { Text("Slider B") }
      Button {
        showOptionA.toggle()
      } label: {
        if showOptionA { Text("Option A") } else { Text("Option B") }
      }
    }
    .animation(.default, value: showOptionA)
  }
}

Expected

  • Tapping the button toggles showOptionA and immediately swaps the displayed "Slider".
  • No AttributeGraph cycle warnings.

Actual

  • The label fails to update.
  • These errors are logged in the console:
=== AttributeGraph: cycle detected through attribute 126800 ===
=== AttributeGraph: cycle detected through attribute 29376 ===
=== AttributeGraph: cycle detected through attribute 126800 ===
=== AttributeGraph: cycle detected through attribute 126800 ===
=== AttributeGraph: cycle detected through attribute 126800 ===
=== AttributeGraph: cycle detected through attribute 32488 ===
=== AttributeGraph: cycle detected through attribute 29376 ===
=== AttributeGraph: cycle detected through attribute 32488 ===
=== AttributeGraph: cycle detected through attribute 29376 ===
=== AttributeGraph: cycle detected through attribute 30024 ===
=== AttributeGraph: cycle detected through attribute 29376 ===
=== AttributeGraph: cycle detected through attribute 32488 ===
=== AttributeGraph: cycle detected through attribute 29376 ===
=== AttributeGraph: cycle detected through attribute 30024 ===
=== AttributeGraph: cycle detected through attribute 29376 ===
=== AttributeGraph: cycle detected through attribute 30024 ===
=== AttributeGraph: cycle detected through attribute 29376 ===
=== AttributeGraph: cycle detected through attribute 32488 ===
=== AttributeGraph: cycle detected through attribute 29376 ===
=== AttributeGraph: cycle detected through attribute 32488 ===

The "Slider" is a placeholder Text element because any use of Slider seems to trigger the same warnings.

The same SliderPlaybackView works fine in an .overlay().

Simplifying SliderPlaybackView in this manner seems to avoid the cycles in the tabViewBottomAccessory:

struct SliderPlaybackView: View {
  @State private var showOptionA: Bool = false
  var body: some View {
    HStack {
      Text(showOptionA ? "Slider A" : "Slider B")
      Text(showOptionA ? "Option A" : "Option B")
        .onTapGesture {
          showOptionA.toggle()
        }
    }
    .animation(.default, value: showOptionA)
  }
}

The seemingly limited functionality available in tabViewBottomAccessory's Content closure requires a lot of trial and error to discover and significantly increases the level of effort required to adopt this new API.

I'm also getting this, and without a fix I cannot release an updated version of my app.

I've also experienced this. Putting a Button or Image inside .tabViewBottomAccessory leads to AttributeGraph cycles and causes it to not update properly.

Makes it pretty much unusable. Is there a workaround until this is fixed?

Ok, I might've found a (dumb) workaround:

  1. Rebuild your view inside .tabViewBottomAccessory using UIKit
  2. Wrap it using UIViewRepresentable
  3. Use the new wrapped view inside .tabViewBottomAccessory

Seems to be working, at least I'm not getting AttributeGraph warnings anymore and the re-renders are instant 🙃

Same. I find out a solution to avoid it by putting conditions outside like below, but this will lead no animations.

.tabViewBottomAccessory {
     if A {
          ViewA()
     }else{
         ViewB()
     }
}

I don't think they were really ready to release these new APIs...

tabViewBottomAccessory AttributeGraph cycles, broken behavior of views participating in cycles
 
 
Q