Leon . Kang
Back to articles
πŸ“± Coding β€’ 2/3/2026 β€’ 10 min read

Bug Analysis: SwiftUI navigationTransition Issues in Real-World Projects

Background: Hero Transition Requirement in EXIF Master

During the development of EXIF Master, I wanted to implement a Hero Transition effect between the photo list view and the photo detail view. SwiftUI has introduced the following APIs in recent versions:

  • .navigationTransition(.zoom)
  • .matchedTransitionSource

Judging by the API design, this is the official standard solution.

The goal behavior is clear:

  1. List view displays photo thumbnails.
  2. Tapping a photo triggers a push to the detail view with a zoom animation.
  3. Use the system's swipe-back gesture to return.
  4. After returning, the original photo in the list should remain visible.

However, the actual behavior did not meet expectations.


The Issue: Source Photo Disappears After Gesture Return

The phenomenon reproduced on real devices and simulators is as follows:

  • Animation works correctly when pushing to the photo detail view.
  • Returning via the back button works correctly in most cases.
  • After returning using the interactive pop gesture (swipe back):
    • The corresponding photo in the original list directly disappears.
    • It's not a state loss, but the view becomes invisible.
  • The issue disappears immediately after removing all code related to navigationTransition / matchedTransitionSource.

This implies:

The issue is unrelated to data, state, or Diffable updates, but is strongly related to the animation mechanism.


Investigation: Why This Isn't "My Fault"

Before confirming it was a framework issue, I performed the following checks:

  • Confirmed the data source remained unchanged (stable Array, ID, and Binding).
  • Attempted to force refresh the view using .id().
  • Replaced Image with Color / Rectangle.
  • Removed lazy loading containers (LazyVGrid / LazyVStack).
  • Disabled animations, transactions, and withAnimation.

The conclusion was consistent:

The problem disappears as soon as navigationTransition is removed.

This behavior highly matches the characteristics of a "framework-level rendering or lifecycle bug."


Final Confirmation: It IS an Official SwiftUI Bug

After describing the complete reproduction conditions to GPT, the conclusion led to:

  • This is a known issue with SwiftUI NavigationStack + navigationTransition.
  • It is not an isolated case.
  • It has been repeatedly reported in the developer community.
  • No explicit fix has been seen in release notes to date.

Affected System Versions

Based on feedback from Reddit, Stack Overflow, and the Apple Developer Forum:

iOS Version Affected Note
iOS 18.x ❌ No Works normally
iOS 26.0 (Beta / Release) βœ… Yes First widely reported
iOS 26.1 Beta / RC βœ… Yes Not fixed
iOS 26.1 Official βœ… Yes Community continues to report
iOS 26.2 & later ⚠️ Likely No explicit fix confirmed yet

Core characteristics:

  • Only appears in the iOS 26 series.
  • Strongly correlated with interactive pop (swipe back).
  • Related to failure in restoring matchedTransitionSource.

Technical Cause (Speculation)

Combining community analysis, reasonable inferences include:

  1. Internal implementation changes to NavigationStack in iOS 26.
  2. matchedTransitionSource needs to "backfill" the source view state after the animation ends.
  3. When the animation is interrupted or ended early by a gesture:
    • The source view is not correctly re-registered to the render tree.
    • Resulting in the view existing but not participating in drawing.

This is not an API misuse, but a missing lifecycle restoration path.


Workaround (Imperfect)

Option 1: Use fullScreenCover instead of push

Move the detail view out of the NavigationStack:

@State private var showDetail = false
@Namespace private var ns

var body: some View {
    Image(...)
        .matchedTransitionSource(id: photo.id, in: ns)
        .onTapGesture {
            showDetail = true
        }
        .fullScreenCover(isPresented: $showDetail) {
            DetailView(photo: photo)
                .navigationTransition(
                    .zoom(sourceID: photo.id, in: ns)
                )
        }
}

Pros:

  • Avoids the NavigationStack bug.
  • Animation is still usable.

Cons:

  • Loses system-level push / pop semantics.
  • Closing logic needs to be handled manually.
  • UX is not entirely consistent with native navigation.

Why This Issue Was "Slippery"

This bug consumes significant developer time because:

  • The behavior looks like a "state management error."
  • Logs, breakpoints, and debug tools cannot point directly to the problem.
  • Example code often fails to reproduce it in simple demos.
  • It only appears consistently under real interaction (gesture return).

This is why I spent a long time troubleshooting before deleting the animation.


The Value of AI in Coding and Troubleshooting

The value of AI in programming and troubleshooting has become very clear. Previously, such issues often required:

  • Browsing through massive amounts of forums and issues.
  • Manually comparing different system versions.
  • Step-by-step elimination to approach the conclusion.

Now, AI can quickly identify "this is not your problem, but a framework issue" even with highly scattered information, providing version ranges, cause inference, and feasible alternatives. This efficiency was unimaginable in the past.

Whether as a collaboration tool or for directly delegating implementation after defining boundaries, the development process has been significantly accelerated. Developers can thus release their time from meaningless repetitive debugging and turn to thinking about truly important questions: Is the product function reasonable? Does the interaction fit real human usage habits? And how does the system perform in a real environment?