Summary
A SwiftUI toolbar using the new iOS 26 Liquid Glass animation only animates when its content changes rapidly. Under normal state changes—such as switching a segmented picker—the toolbar updates instantly with no morphing/materialize animation. This postmortem explains why this happens, why it’s expected in real systems, and how senior engineers work around it.
Root Cause
The Liquid Glass animation engine in iOS 26 is tied to navigation-driven transitions, not arbitrary state changes. SwiftUI only triggers the morph animation when it detects:
- A navigation event (push/pop)
- A rapid sequence of mutations that resemble a transition
- A layout diff large enough to qualify as an animated morph
Segmented picker changes update the toolbar too cleanly and too predictably, so the system treats them as static content swaps rather than animated transitions.
Why This Happens in Real Systems
Real UI frameworks optimize for stability and performance. Apple’s Liquid Glass engine is designed around:
- Predictable navigation flows, not arbitrary state-driven toolbar mutations
- Heuristics that decide when an animation is “worth it”
- Debouncing logic that suppresses animations when changes appear intentional and non-transitional
- Performance safeguards that avoid animating expensive toolbar diffs unless necessary
When switching tabs slowly, the system concludes:
“This is a normal state update, not a transition—skip the animation.”
Real-World Impact
This leads to several observable behaviors:
- Toolbar morphing only appears during rapid changes, which resemble a transition
- Slow or deliberate state changes bypass the animation
- Developers cannot rely on Liquid Glass for non-navigation toolbar updates
- UI feels inconsistent when toolbar content changes outside navigation
Example or Code (if necessary and relevant)
A minimal reproduction of the issue:
struct ContentView: View {
@State private var tab: Tabs = .tab1
var body: some View {
NavigationStack {
ZStack {
switch tab {
case .tab1:
Text("Tab 1")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Tab 1 Button", systemImage: "person") { }
}
}
case .tab2:
Text("Tab 2")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Tab 2 Button", systemImage: "plus") { }
}
ToolbarItem(placement: .topBarLeading) {
Button("Tab 2 Button", systemImage: "trash") { }
}
}
}
}
.animation(.default, value: tab)
.navigationTitle("Testing Toolbar")
.toolbarTitleDisplayMode(.inline)
.safeAreaBar(edge: .top) {
Picker("Picker", selection: $tab) {
Text("Tab 1").tag(Tabs.tab1)
Text("Tab 2").tag(Tabs.tab2)
}
.pickerStyle(.segmented)
}
}
}
}
enum Tabs { case tab1, tab2 }
How Senior Engineers Fix It
Senior engineers recognize that Liquid Glass cannot be forced directly, so they use one of these strategies:
- Wrap toolbar changes in a synthetic navigation transition
- e.g., embed content in a child view that pushes/pops on state change
- Trigger a micro-transition by:
- temporarily inserting a placeholder toolbar item
- delaying the real update by a few milliseconds
- Use custom animations to replicate the morphing effect when toolbar content changes
- Move state-driven toolbar changes into navigation flows, where Liquid Glass is guaranteed to animate
- File a Feedback/Radar because this behavior is undocumented and likely intentional but restrictive
The key insight:
You cannot force Liquid Glass directly; you must shape your UI so the system chooses to animate.
Why Juniors Miss It
Juniors often assume:
- All toolbar changes animate automatically
.animation(.default, value:)applies to toolbars- Liquid Glass is a general-purpose animation engine
- State changes and navigation transitions behave the same
They miss that Apple’s animation heuristics are contextual, not universal. Without understanding the underlying transition model, it’s easy to expect animations where the system never intended them.
If you want, I can sketch a workaround pattern that reliably triggers Liquid Glass for non-navigation toolbar changes.