Introduction
The warning "Presenting view controllers on detached view controllers is discouraged" appears when you call present(_:animated:completion:) on a view controller that is not part of the active view controller hierarchy. A view controller is "detached" when it has been removed from its parent, dismissed, or not yet added to the window's root hierarchy. The fix is to ensure you always present from the topmost visible view controller — typically the root view controller or the currently presented controller.
What Causes the Warning
1class ViewController: UIViewController {
2 override func viewDidLoad() {
3 super.viewDidLoad()
4
5 // This may trigger the warning if called before
6 // the view controller is fully in the hierarchy
7 let alert = UIAlertController(title: "Hello", message: "World", preferredStyle: .alert)
8 alert.addAction(UIAlertAction(title: "OK", style: .default))
9 present(alert, animated: true) // Warning if self is detached
10 }
11}
Calling present() in viewDidLoad() is risky because the view controller may not yet be added to the window hierarchy. The view is loaded but the controller may not be "attached" to the display chain.
Fix 1: Present in viewDidAppear
1class ViewController: UIViewController {
2 override func viewDidAppear(_ animated: Bool) {
3 super.viewDidAppear(animated)
4
5 // Safe — the view controller is fully in the hierarchy
6 let alert = UIAlertController(title: "Welcome", message: "You are logged in", preferredStyle: .alert)
7 alert.addAction(UIAlertAction(title: "OK", style: .default))
8 present(alert, animated: true)
9 }
10}
viewDidAppear is called after the view controller is visible and part of the active hierarchy. This is the safest lifecycle method for presenting other view controllers.
Fix 2: Present from the Top View Controller
1extension UIViewController {
2 static var topMost: UIViewController? {
3 var topController = UIApplication.shared.connectedScenes
4 .compactMap { ($0 as? UIWindowScene)?.keyWindow?.rootViewController }
5 .first
6
7 while let presented = topController?.presentedViewController {
8 topController = presented
9 }
10
11 // Handle navigation and tab controllers
12 if let nav = topController as? UINavigationController {
13 topController = nav.visibleViewController
14 }
15 if let tab = topController as? UITabBarController {
16 topController = tab.selectedViewController
17 }
18
19 return topController
20 }
21}
22
23// Usage from anywhere
24UIViewController.topMost?.present(alertController, animated: true)
This utility finds the topmost visible view controller by traversing the presentation chain from the root. It handles navigation controllers, tab controllers, and modally presented controllers.
Fix 3: Present from the Root View Controller
1// Simple approach — present from the window's root
2func presentFromRoot(_ viewController: UIViewController, animated: Bool = true) {
3 guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
4 let rootVC = windowScene.windows.first?.rootViewController else {
5 return
6 }
7
8 // Dismiss any existing presentation first
9 if rootVC.presentedViewController != nil {
10 rootVC.dismiss(animated: false) {
11 rootVC.present(viewController, animated: animated)
12 }
13 } else {
14 rootVC.present(viewController, animated: animated)
15 }
16}
Presenting from the root view controller works when you do not have a deep presentation chain. If another controller is already presented, dismiss it first.
Common Scenario: Presenting After Dismiss
1class ParentVC: UIViewController {
2 func showFirst() {
3 let firstVC = FirstViewController()
4 firstVC.onDismiss = { [weak self] in
5 // BAD: presenting immediately after dismiss — firstVC may still be detached
6 let secondVC = SecondViewController()
7 self?.present(secondVC, animated: true)
8 }
9 present(firstVC, animated: true)
10 }
11}
12
13class FirstViewController: UIViewController {
14 var onDismiss: (() -> Void)?
15
16 func done() {
17 dismiss(animated: true) { [weak self] in
18 // GOOD: present in the dismiss completion handler
19 self?.onDismiss?()
20 }
21 }
22}
Always present the next view controller in the dismiss completion handler. Presenting before the dismissal animation completes causes the detached warning because the presenting controller is still mid-transition.
Common Scenario: Presenting from a Child View Controller
1// BAD: child view controller presenting directly
2class ChildVC: UIViewController {
3 func showAlert() {
4 let alert = UIAlertController(title: "Error", message: "Something failed", preferredStyle: .alert)
5 alert.addAction(UIAlertAction(title: "OK", style: .default))
6 // This may warn if ChildVC is embedded but not the presenting controller
7 present(alert, animated: true)
8 }
9}
10
11// GOOD: delegate to the parent or use the top view controller
12class ChildVC: UIViewController {
13 func showAlert() {
14 let alert = UIAlertController(title: "Error", message: "Something failed", preferredStyle: .alert)
15 alert.addAction(UIAlertAction(title: "OK", style: .default))
16
17 // Present from the nearest non-detached ancestor
18 let presenter = parent ?? self
19 presenter.present(alert, animated: true)
20 }
21}
Child view controllers added via addChild() are in the hierarchy but may not be the right presenter. Use parent to delegate presentation upward.
Debugging the Issue
1// Check if a view controller is in the hierarchy
2func isInHierarchy(_ vc: UIViewController) -> Bool {
3 return vc.viewIfLoaded?.window != nil
4}
5
6// Log the view controller chain
7func printHierarchy() {
8 var vc = UIApplication.shared.connectedScenes
9 .compactMap { ($0 as? UIWindowScene)?.keyWindow?.rootViewController }
10 .first
11
12 var depth = 0
13 while let current = vc {
14 print("\(String(repeating: " ", count: depth))\(type(of: current))")
15 if let nav = current as? UINavigationController {
16 print("\(String(repeating: " ", count: depth + 1))Stack: \(nav.viewControllers.map { type(of: $0) })")
17 }
18 vc = current.presentedViewController
19 depth += 1
20 }
21}
Check viewIfLoaded?.window != nil to verify a view controller is attached to a window. If it is nil, the controller is detached and should not present other controllers.
Common Pitfalls
Presenting in viewDidLoad or viewWillAppear: The view controller may not be fully in the hierarchy during these lifecycle methods. Wait until viewDidAppear to present other controllers.
Not using the dismiss completion handler: Calling present() immediately after dismiss() without waiting for the completion block causes the warning. Always present in the completion closure of dismiss.
Presenting from a view controller that was popped from a navigation stack: After popViewController, the popped controller is detached. Any delayed callbacks (network responses, timers) that try to present from it trigger the warning.
Multiple scenes on iPad: On iPad with multiple scenes, UIApplication.shared.keyWindow is deprecated and may return the wrong window. Use UIWindowScene to find the correct window for the active scene.
Ignoring the warning: While the presentation may still appear to work, detached presentations can cause visual glitches, broken transitions, and crashes on certain iOS versions. Always fix the warning.
Summary
The warning means you are presenting from a view controller not in the active hierarchy
Present in viewDidAppear, not viewDidLoad or viewWillAppear
Use a topMost view controller utility to find the correct presenter
Always present the next controller in the dismiss completion handler
Check viewIfLoaded?.window != nil to verify a controller is attached
Delegate presentation upward using parent when presenting from child view controllers