Leaking views when changing rootViewController inside transitionWithView
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Animating a rootViewController swap is common for login transitions, onboarding completion, and account switching. When the old interface does not deallocate afterward, the problem is usually not UIView.transition itself but some other object still retaining the previous controller tree. The fix is to combine a clean root replacement with disciplined ownership cleanup and leak verification.
What Should Happen During a Root Swap
When a window gets a new rootViewController, the previous root and its view hierarchy should eventually be released. That only happens if nothing else still owns them.
Common retainers include:
- coordinators storing old navigation stacks
- timers or display links targeting the old controller
- strong delegate references
- closures capturing controllers strongly
- observers or subscriptions that were never removed or cancelled
If any of those remain alive, the old root graph stays in memory even though it is no longer visible.
A Safe Transition Pattern
Keep the transition code small and do the root replacement directly on the window.
This code avoids holding unnecessary references to the previous root inside completion handlers or custom animation objects.
Coordinators Are a Frequent Leak Source
In coordinator-based apps, root transitions often leave old flows alive because the parent coordinator still retains them.
Example pattern to audit:
If the old login coordinator or navigation controller is still stored somewhere, the transition appears to leak even though the root swap is correct.
Closures and Async Work Can Keep Old UI Alive
Any async callback that captures the old controller strongly can delay or prevent deallocation.
Use weak captures when a callback outlives the current screen. This is especially important around network requests, animations, and delayed transitions.
Timers, Observers, and Subscriptions
Hidden retain cycles often come from long-lived infrastructure objects. Check these first:
- '
Timer.scheduledTimer' - '
CADisplayLink' - '
NotificationCenterobservers' - Combine subscriptions
- delegate properties that should be
weak
Example cleanup:
If deinit never runs, the timer or another retainer is still active.
Debug With the Memory Graph, Not Guesswork
The fastest way to diagnose the leak is with Xcode Memory Graph Debugger.
Suggested workflow:
- run the app and trigger the root swap
- wait until the new root is fully visible
- capture the memory graph
- search for the old controller type
- inspect the retain path back to the owner
This shows the exact object preventing release, which is more reliable than assuming the transition API is at fault.
Snapshot-Based Transitions Can Simplify Ownership
If you want more control, replace the root immediately and animate a snapshot instead of the old controller tree.
High-level flow:
- snapshot current window
- set new root without animation
- place snapshot above new root
- fade out snapshot and remove it
That approach decouples animation from controller ownership and can make leak debugging simpler.
Common Pitfalls
- Blaming
UIView.transitionwhen the real leak is a retained coordinator or timer. Fix: inspect retain paths and clean ownership explicitly. - Capturing old controllers strongly in async closures. Fix: use weak captures for callbacks that can outlive the screen.
- Forgetting to cancel observers, timers, or subscriptions. Fix: tear them down in lifecycle or
deinit. - Keeping global references to old navigation stacks. Fix: clear stored coordinator and controller references during the flow switch.
- Verifying only visually and never checking deallocation. Fix: use memory graph and temporary
deinitlogging during debugging.
Summary
- Root swaps should release the previous controller tree once all strong references are gone.
- Most leaks come from ownership issues, not from the transition call itself.
- Keep the transition code minimal and replace the root directly on the window.
- Audit coordinators, closures, timers, delegates, and subscriptions.
- Use Xcode Memory Graph to identify the real retainer before changing architecture.

