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:
- List view displays photo thumbnails.
- Tapping a photo triggers a push to the detail view with a zoom animation.
- Use the system's swipe-back gesture to return.
- 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
ImagewithColor / 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:
- Internal implementation changes to
NavigationStackin iOS 26. matchedTransitionSourceneeds to "backfill" the source view state after the animation ends.- 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?