Search code examples
iosswiftcocoa-touchcrashpopup

Present UIAlertController on top of everything regardless of the view hierarchy


I'm trying to have an helper class that presents an UIAlertController. Since it's a helper class, I want it to work regardless of the view hierarchy, and with no information about it. I'm able to show the alert, but when it's being dismissed, the app crashed with:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Trying to dismiss UIAlertController <UIAlertController: 0x135d70d80>
 with unknown presenter.'

I'm creating the popup with:

guard let window = UIApplication.shared.keyWindow else { return }
let view = UIView()
view.isUserInteractionEnabled = true
window.insertSubview(view, at: 0)
window.bringSubview(toFront: view)
// add full screen constraints to view ...

let controller = UIAlertController(
  title: "confirm deletion?",
  message: ":)",
  preferredStyle: .alert
)

let deleteAction = UIAlertAction(
  title: "yes",
  style: .destructive,
  handler: { _ in
    DispatchQueue.main.async {
      view.removeFromSuperview()
      completion()
    }
  }
)
controller.addAction(deleteAction)

view.insertSubview(controller.view, at: 0)
view.bringSubview(toFront: controller.view)
// add centering constraints to controller.view ...

When I tap yes, the app will crash and the handler is not being hit before the crash. I can't present the UIAlertController because this would be dependent of the current view hierarchy, while I want the popup to be independant

EDIT: Swift solution Thanks @Vlad for the idea. It seems that operating in a separate window is much more simple. So here is a working Swift solution:

class Popup {
  private var alertWindow: UIWindow
  static var shared = Popup()

  init() {
    alertWindow = UIWindow(frame: UIScreen.main.bounds)
    alertWindow.rootViewController = UIViewController()
    alertWindow.windowLevel = UIWindowLevelAlert + 1
    alertWindow.makeKeyAndVisible()
    alertWindow.isHidden = true
  }

  private func show(completion: @escaping ((Bool) -> Void)) {
    let controller = UIAlertController(
      title: "Want to do it?",
      message: "message",
      preferredStyle: .alert
    )

    let yesAction = UIAlertAction(
      title: "Yes",
      style: .default,
      handler: { _ in
        DispatchQueue.main.async {
          self.alertWindow.isHidden = true
          completion(true)
        }
    })

    let noAction = UIAlertAction(
      title: "Not now",
      style: .destructive,
      handler: { _ in
        DispatchQueue.main.async {
          self.alertWindow.isHidden = true
          completion(false)
        }
    })

    controller.addAction(noAction)
    controller.addAction(yesAction)
    self.alertWindow.isHidden = false
    alertWindow.rootViewController?.present(controller, animated: false)
  }
}

Solution

  • Update Dec 16, 2019:

    Just present the view controller/alert from the current top-most view controller. That will work :)

    if #available(iOS 13.0, *) {
         if var topController = UIApplication.shared.keyWindow?.rootViewController  {
               while let presentedViewController = topController.presentedViewController {
                     topController = presentedViewController
                    }
         topController.present(self, animated: true, completion: nil)
    }
    

    Update July 23, 2019:

    IMPORTANT

    Apparently the method below this technique stopped working in iOS 13.0 :(

    I'll update once I find the time to investigate...

    Old technique:

    Here's a Swift (5) extension for it:

    public extension UIAlertController {
        func show() {
            let win = UIWindow(frame: UIScreen.main.bounds)
            let vc = UIViewController()
            vc.view.backgroundColor = .clear
            win.rootViewController = vc
            win.windowLevel = UIWindow.Level.alert + 1  // Swift 3-4: UIWindowLevelAlert + 1
            win.makeKeyAndVisible()    
            vc.present(self, animated: true, completion: nil)
        }
    }
    

    Just setup your UIAlertController, and then call:

    alert.show()
    

    No more bound by the View Controllers hierarchy!