39

I am upgrading my app to use the new UIScene patterns as defined in iOS 13, however a critical part of the app has stopped working. I have been using a UIWindow to cover the current content on the screen and present new information to the user, but in the current beta I am working with (iOS + XCode beta 3) the window will appear, but then disappear straight away.

Here is the code I was using, that now does not work:

let window = UIWindow(frame: UIScreen.main.bounds)
let viewController = UIViewController()
viewController.view.backgroundColor = .clear
window.rootViewController = viewController
window.windowLevel = UIWindow.Level.statusBar + 1
window.makeKeyAndVisible()
viewController.present(self, animated: true, completion: nil)

I have tried many things, including using WindowScenes to present the new UIWindow, but cannot find any actual documentation or examples out there.

One of my attempts (Did not work - same behaviour with window appearing and dismissing straight away)

let windowScene = UIApplication.shared.connectedScenes.first
if let windowScene = windowScene as? UIWindowScene {
    let window = UIWindow(windowScene: windowScene)
    let viewController = UIViewController()
    viewController.view.backgroundColor = .clear
    window.rootViewController = viewController
    window.windowLevel = UIWindow.Level.statusBar + 1
    window.makeKeyAndVisible()
    viewController.present(self, animated: true, completion: nil)
}

Has anyone been able to do this yet in iOS 13 beta?

Thanks

EDIT

Some time has passed between asking this and the final version of iOS 13 being released. There are a lot of answers below, but almost all of them include one thing - Adding a strong/stronger reference to the UIWindow. You may need to include some code relating the the new Scenes, but try adding the strong reference first.

mHopkins
  • 500
  • 1
  • 4
  • 10
  • 2
    you need to keep a reference to your window object – Leo Dabus Jul 16 '19 at 15:28
  • 1
    @LeoDabus I am trying to present a new window over the current one though? Where would I use the reference to my window object? The top code block worked perfectly before the 13 beta – mHopkins Jul 16 '19 at 15:37
  • 1
    Move the window declaration out of the closure – Leo Dabus Jul 16 '19 at 15:38
  • 1
    try `var window: UIWindow?` `if let windowScene = windowScene as? UIWindowScene {` `window = .init(windowScene: windowScene)` then use optional chaining `window?.whatever` – Leo Dabus Jul 16 '19 at 15:43
  • @LeoDabus Ahhh I understand. I tried that, but it did not work. `Window` and `windowScene` are both actual objects that have been successfully referenced, the issue isn't that it can't find those objects, I just get the same behaviour as the top code block with the bottom code block. Thanks – mHopkins Jul 16 '19 at 15:48
  • @LeoDabus Thank you for your suggestion to keep a reference to the window. That solved the same problem for me! – Brian Boyle Aug 28 '19 at 15:14

12 Answers12

33

I was experiencing the same problems while upgrading my code for iOS 13 scenes pattern. With parts of your second code snippet I managed to fix everything so my windows are appearing again. I was doing the same as you except for the last line. Try removing viewController.present(...). Here's my code:

let windowScene = UIApplication.shared
                .connectedScenes
                .filter { $0.activationState == .foregroundActive }
                .first
if let windowScene = windowScene as? UIWindowScene {
    popupWindow = UIWindow(windowScene: windowScene)
}

Then I present it like you do:

popupWindow?.frame = UIScreen.main.bounds
popupWindow?.backgroundColor = .clear
popupWindow?.windowLevel = UIWindow.Level.statusBar + 1
popupWindow?.rootViewController = self as? UIViewController
popupWindow?.makeKeyAndVisible()

Anyway, I personally think that the problem is in viewController.present(...), because you show a window with that controller and immediately present some 'self', so it depends on what 'self' really is.

Also worth mentioning that I store a reference to the window you're moving from inside my controller. If this is still useless for you I can only show my small repo that uses this code. Have a look inside AnyPopupController.swift and Popup.swift files.

Hope that helps, @SirOz

glassomoss
  • 526
  • 4
  • 6
  • 2
    it does not overlay status bar – Neil Galiaskarov Oct 08 '19 at 07:24
  • Just to call this out, because it took me a few passes to notice it: the main difference relevant to my app is that the popup window should be initialized with the appropriate window scene: `UIWindow(windowScene: windowScene)`. Everything else in my app remained the same. Great job! – Wayne Feb 07 '20 at 22:59
13

Based on all the proposed solutions, I can offer my own version of the code:

private var window: UIWindow!

extension UIAlertController {
    func present(animated: Bool, completion: (() -> Void)?) {
        window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = UIViewController()
        window.windowLevel = .alert + 1
        window.makeKeyAndVisible()
        window.rootViewController?.present(self, animated: animated, completion: completion)
    }

    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        window = nil
    }
}

How to use:

// Show message (from any place)
let alert = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Button", style: .cancel))
alert.present(animated: true, completion: nil)
Vergiliy
  • 958
  • 7
  • 18
6

Here are the steps to present a view controller in a new window on iOS 13:

  1. Detect focused UIWindowScene.
extension UIWindowScene {
    static var focused: UIWindowScene? {
        return UIApplication.shared.connectedScenes
            .first { $0.activationState == .foregroundActive && $0 is UIWindowScene } as? UIWindowScene
    }
}
  1. Create UIWindow for the focused scene.
if let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) {
  // ...
}
  1. Present UIViewController in that window.
let myViewController = UIViewController()

if let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) {
    window.rootViewController = myViewController
    window.makeKeyAndVisible()
}
Vadim Bulavin
  • 2,991
  • 21
  • 17
4

Thank you @glassomoss. My problem is with UIAlertController.

I solved my problem in this way:

  • I added a variable
var windowsPopUp: UIWindow?
  • I modified the code to display the PopUp:
public extension UIAlertController {
    func showPopUp() {
        windowsPopUp = UIWindow(frame: UIScreen.main.bounds)
        let vc = UIViewController()
        vc.view.backgroundColor = .clear
        windowsPopUp!.rootViewController = vc
        windowsPopUp!.windowLevel = UIWindow.Level.alert + 1
        windowsPopUp!.makeKeyAndVisible()
        vc.present(self, animated: true)
    }
}
  • In the action of the UIAlertController I added:
windowsPopUp = nil

without the last line the PopUp is dismissed but the windows remains active not allowing the iteration with the application (with the application window)

SirOz
  • 421
  • 3
  • 5
  • 2
    Another fun iOS 13 bug, if you add a second UIWindow like this, all touches get eaten, even with userInteractionEnabled = NO... – jjxtra Sep 19 '19 at 21:01
  • @jjxtra a workaround is to set hidden to YES. Not ideal but works. – RunLoop Sep 22 '19 at 04:36
  • The solution is to set an empty view controller with transparent view and user interaction disabled as the root view controller of the new UIWindow. Without this everything breaks, but only on iOS 13. On iOS 12 and earlier, you could set the same root view controller on both windows. – jjxtra Sep 22 '19 at 15:46
4

You just need to store strong reference of UIWindow that you want to present. It seems that under the hood view controller that presented does not references to the window.

Andrey M.
  • 2,487
  • 20
  • 38
3

As everyone else mentioned, the issue is that a strong reference to the window is required. So to make sure that this window is removed again after use, I encapsulated everything needed in it's own class..

Here's a little Swift 5 snippet:

class DebugCheatSheet {

    private var window: UIWindow?

    func present() {
        let vc = UIViewController()
        vc.view.backgroundColor = .clear

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = vc
        window?.windowLevel = UIWindow.Level.alert + 1
        window?.makeKeyAndVisible()

        vc.present(sheet(), animated: true, completion: nil)
    }

    private func sheet() -> UIAlertController {
        let alert = UIAlertController.init(title: "Cheatsheet", message: nil, preferredStyle: .actionSheet)
        addAction(title: "Ok", style: .default, to: alert) {
            print("Alright...")
        }
        addAction(title: "Cancel", style: .cancel, to: alert) {
            print("Cancel")
        }
        return alert
    }

    private func addAction(title: String?, style: UIAlertAction.Style, to alert: UIAlertController, action: @escaping () -> ()) {
        let action = UIAlertAction.init(title: title, style: style) { [weak self] _ in
            action()
            alert.dismiss(animated: true, completion: nil)
            self?.window = nil
        }
        alert.addAction(action)
    }
}

And here is how I use it.. It's from the lowest view controller in the whole apps view hierarchy, but could be used from anywhere else also:

private let cheatSheet = DebugCheatSheet()

override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
    if motion == .motionShake {
        cheatSheet.present()
    }
}
Wiingaard
  • 3,332
  • 3
  • 26
  • 58
1

iOS 13 broke my helper functions for managing alerts.

Because there may be cases where you need multiple alerts to be displayed at the same time (the most recent above the older) for example in case you're displaying a yes or no alert and in the meanwhile your webservice returns with a error you display via an alert (it's a limit case but it can happen),

my solution is to extend the UIAlertController like this, and let it have its own alertWindow to be presented from.

The pro is that when you dismiss the alert the window is automatically dismissed because there is any strong reference left, so no further mods to be implemented.

Disclaimer : I just implemented it, so I still need to see if it's consistent...

class AltoAlertController: UIAlertController {

var alertWindow : UIWindow!

func show(animated: Bool, completion: (()->(Void))?)
{
    alertWindow = UIWindow(frame: UIScreen.main.bounds)
    alertWindow.rootViewController = UIViewController()
    alertWindow.windowLevel = UIWindow.Level.alert + 1
    alertWindow.makeKeyAndVisible()
    alertWindow.rootViewController?.present(self, animated: animated, completion: completion)
}

}

Il Pisanello
  • 97
  • 2
  • 4
1

Here's a bit hacky way of holding a strong reference to created UIWindow and releasing it after presented view controller is dismissed and deallocated. Just make sure you don't make reference cycles.

private final class WindowHoldingViewController: UIViewController {

    private var window: UIWindow?

    convenience init(window: UIWindow) {
        self.init()

        self.window = window
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor.clear
    }

    override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
        let view = DeallocatingView()
        view.onDeinit = { [weak self] in
            self?.window = nil
        }
        viewControllerToPresent.view.addSubview(view)

        super.present(viewControllerToPresent, animated: flag, completion: completion)
    }

    private final class DeallocatingView: UIView {

        var onDeinit: (() -> Void)?

        deinit {
            onDeinit?()
        }
    }
}

Usage:

let vcToPresent: UIViewController = ...
let window = UIWindow() // or create via window scene
...
window.rootViewController = WindowHoldingViewController(window: window)
...
window.rootViewController?.present(vcToPresent, animated: animated, completion: completion)
younke
  • 31
  • 2
1

Need have pointer a created window for ios13.

example my code:

 extension UIAlertController {

    private static var _aletrWindow: UIWindow?
    private static var aletrWindow: UIWindow {
        if let window = _aletrWindow {
            return window
        } else {
            let window = UIWindow(frame: UIScreen.main.bounds)
            window.rootViewController = UIViewController()
            window.windowLevel = UIWindowLevelAlert + 1
            window.backgroundColor = .clear
            _aletrWindow = window
            return window
        }
    }

    func presentGlobally(animated: Bool, completion: (() -> Void)? = nil) {
        UIAlertController.aletrWindow.makeKeyAndVisible()
        UIAlertController.aletrWindow.rootViewController?.present(self, animated: animated, completion: completion)
    }

    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        UIAlertController.aletrWindow.isHidden = true
    }

}

use:

let alert = UIAlertController(...
...

alert.presentGlobally(animated: true)
RomanV
  • 419
  • 4
  • 5
1

You can try like this:

extension UIWindow {
    static var key: UIWindow? {
        if #available(iOS 13, *) {
            return UIApplication.shared.windows.first { $0.isKeyWindow }
        } else {
            return UIApplication.shared.keyWindow
        }
    }
}

Usage:

if let rootVC = UIWindow.key?.rootViewController {
    rootVC.present(nextViewController, animated: true, completion: nil)
}

Keep Coding........ :)

Krishna Raj Salim
  • 7,061
  • 5
  • 29
  • 62
0

Swift 4.2 iOS 13 UIAlertController extension

This code full working in iOS 11, 12 and 13

import Foundation
import UIKit

extension UIAlertController{
    private struct AssociatedKeys {
        static var alertWindow = "alertWindow"
    }
    var alertWindow:UIWindow?{
        get{
            guard let alertWindow = objc_getAssociatedObject(self, &AssociatedKeys.alertWindow) as? UIWindow else {
                return nil
            }
            return alertWindow
        }
        set(value){
            objc_setAssociatedObject(self,&AssociatedKeys.alertWindow,value,objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    func show(animated:Bool) {
        self.alertWindow = UIWindow(frame: UIScreen.main.bounds)
        self.alertWindow?.rootViewController = UIViewController()
        self.alertWindow?.windowLevel = UIWindow.Level.alert + 1
        if #available(iOS 13, *){
            let mySceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate
            mySceneDelegate!.window?.rootViewController?.present(self, animated: animated, completion: nil)
        }
        else{
            self.alertWindow?.makeKeyAndVisible()
            self.alertWindow?.rootViewController?.present(self, animated: animated, completion: nil)
        }
    }
}
0

In addition to the answers about creating a reference to UIWindow and then presenting modally, I've included a section of my code on how I'm dismissing it.

class PresentingViewController: UIViewController {
    private var coveringWindow: UIWindow?

  func presentMovie() {
    let playerVC = MoviePlayerViewController()
    playerVC.delegate = self
    playerVC.modalPresentationStyle = .overFullScreen
    playerVC.modalTransitionStyle = .coverVertical

    self.coverPortraitWindow(playerVC)
  }

  func coverPortraitWindow(_ movieController: MoviePlayerViewController) {

    let windowScene = UIApplication.shared
        .connectedScenes
        .filter { $0.activationState == .foregroundActive }
        .first
    if let windowScene = windowScene as? UIWindowScene {
        self.coveringWindow = UIWindow(windowScene: windowScene)

        let rootController = UIViewController()
        rootController.view.backgroundColor = .clear

        self.coveringWindow!.windowLevel = .alert + 1
        self.coveringWindow!.isHidden = false
        self.coveringWindow!.rootViewController = rootController
        self.coveringWindow!.makeKeyAndVisible()

        rootController.present(movieController, animated: true)
    }
  }

  func uncoverPortraitWindow() {
    guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
        let sceneDelegate = windowScene.delegate as? SceneDelegate
        else {
            return
    }
    sceneDelegate.window?.makeKeyAndVisible()
    self.coveringWindow = nil
  }

}
PostCodeism
  • 1,030
  • 1
  • 11
  • 19