iOS 13 中 UIWindow 在内容上方未显示

48

我正在升级我的应用程序,以使用iOS 13中定义的新的UIScene模式,但是应用程序的关键部分已经停止工作。 我一直在使用UIWindow来覆盖屏幕上的当前内容,并向用户呈现新信息,但在我目前使用的测试版(iOS + XCode beta 3)中,窗口将出现,但随即消失。

这是我曾经使用的代码,但现在无法工作:

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)

我尝试了许多方法,包括使用 WindowScenes 来呈现新的 UIWindow,但是无法找到实际的文档或示例。

我的其中一次尝试(没有成功 - 窗口出现后立即消失)

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)
}

有人在 iOS 13 beta 中已经成功实现过这个了吗?

谢谢

编辑

问这个问题和最终版本的 iOS 13 发布之间已经过去了一段时间。下面有很多答案,但几乎所有答案都包括一个要点 - 向 UIWindow 添加强引用(strong/stronger reference)。你可能需要包括一些关于新场景(Scenes)的代码,但先尝试添加强引用。


3
你需要保留对窗口对象的引用。 - Leo Dabus
1
@LeoDabus 我正在尝试在当前窗口上呈现一个新窗口?我应该在哪里使用我的窗口对象引用?在13 beta之前,顶部代码块完美地工作。 - mHopkins
1
将窗口声明移出闭包。 - Leo Dabus
1
尝试使用 var window: UIWindow?,如果可以将 windowScene 转换为 UIWindowScene 类型,则执行以下操作:window = .init(windowScene: windowScene)然后使用可选链式调用 window?.whatever - Leo Dabus
@LeoDabus 啊,我明白了。我尝试过了,但是没有成功。WindowwindowScene都是已经成功引用的实际对象,问题不在于找不到这些对象,而是我得到了与顶部代码块相同的行为。谢谢。 - mHopkins
@LeoDabus 谢谢您建议保留对窗口的引用。这为我解决了同样的问题! - Brian Boyle
12个回答

39

在将我的代码升级为iOS 13场景模式时,我遇到了同样的问题。使用你第二段代码片段的一部分,我成功地修复了所有问题,因此我的窗口再次出现了。除了最后一行之外,我和你做的都一样。尝试删除viewController.present(...)。以下是我的代码:

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

那我就像你一样呈现它:

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

无论如何,我个人认为问题在于 viewController.present(...),因为你正在显示一个包含该控制器的窗口,并立即呈现了一些“self”,所以这取决于什么是“self”。

还值得一提的是,我在我的控制器内存储了从中移动的窗口的引用。如果这对您仍然没有用,我只能展示我的小repo,其中使用了此代码。请查看AnyPopupController.swiftPopup.swift文件。

希望这有所帮助,@SirOz


2
它不会覆盖状态栏。 - Neil Galiaskarov
只是想强调一下,因为我需要多次查看才注意到:与我的应用程序相关的主要区别在于,弹出窗口应该使用适当的窗口场景进行初始化:UIWindow(windowScene: windowScene)。我的应用程序中的其他所有内容都保持不变。干得好! - Wayne

14

根据所有提出的解决方案,我可以提供自己的代码版本:

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
    }
}

使用方法:

// 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)

@mHopkins 在 iOS 13 上测试通过! - Vergiliy
是的,正如@andrey-m所指出的那样,您只需要存储要呈现的UIWindow的强引用。 - Ilja Popov
你有没有这样的问题,即在拖动解除时未调用viewDidDisappear? - RealMan
我一直在寻找这个的Objective C版本,但是我在互联网上无法找到它。由于我是IOS开发的新手,我很难用Swift将其转换为Objective C。 - Renascent

9

以下是将视图控制器在iOS 13中呈现在新窗口的步骤:

  1. 检测聚焦的 UIWindowScene
extension UIWindowScene {
    static var focused: UIWindowScene? {
        return UIApplication.shared.connectedScenes
            .first { $0.activationState == .foregroundActive && $0 is UIWindowScene } as? UIWindowScene
    }
}
  1. 为当前场景创建 UIWindow
if let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) {
  // ...
}
  1. 在那个窗口中呈现UIViewController
let myViewController = UIViewController()

if let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) {
    window.rootViewController = myViewController
    window.makeKeyAndVisible()
}

1
这个答案是使用新的场景代理时正确的方法,而其他答案将在iOS 13+上继续工作,只要您不使用新的场景代理,而坚持以前的AppDelegate标准方式即可。 - Justin Ganzer

5
你只需存储要呈现的 UIWindow强引用。似乎在幕后呈现的视图控制器没有对窗口的引用。

你可以在 @Vergiliy 的回答中看到实现。 - Ilja Popov

4
谢谢您 @glassomoss。我的问题与UIAlertController有关。
我是这样解决的:
- 我添加了一个变量
var windowsPopUp: UIWindow?
  • 我修改了代码以显示弹出窗口:
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)
    }
}
  • 在 UIAlertController 中,我添加了以下内容:
windowsPopUp = nil

没有最后一行,弹出窗口会被关闭,但是窗口仍然保持活动状态,不允许与应用程序(应用程序窗口)进行交互。

2
又一个有趣的iOS 13 bug,如果你像这样添加第二个UIWindow,所有的触摸事件都会被吞噬,即使设置了userInteractionEnabled = NO... - jjxtra
1
@jjxtra的一个解决方法是将hidden设置为YES。虽然不是理想的解决方案,但可以起作用。 - RunLoop
解决方案是将一个透明视图和禁用用户交互的空视图控制器设置为新 UIWindow 的根视图控制器。如果没有这个,一切都会崩溃,但只在 iOS 13 上出现问题。在 iOS 12 及更早版本中,您可以在两个窗口上设置相同的根视图控制器。 - jjxtra

3
正如其他人所提到的,问题在于需要一个对窗口的强引用。为了确保在使用后再次删除此窗口,我将需要的所有内容封装在自己的类中。
以下是一个小的 Swift 5 代码片段:
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)
    }
}

以下是我如何使用它的方法。它来自整个应用程序视图层次结构中最低级别的视图控制器,但也可以从任何其他地方使用:

private let cheatSheet = DebugCheatSheet()

override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
    if motion == .motionShake {
        cheatSheet.present()
    }
}

1
你可以尝试像这样:

您可以尝试这样做:

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

使用方法:

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

保持编码……… :)

1
更好的解决方案。代码保持干净 :) - ibyte

1
需要为iOS13创建一个指针指向已创建的窗口。
我的代码示例:
 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)

1
这是一种有点hacky的方法,用于持有创建的UIWindow的强引用,并在呈现的视图控制器被解除引用和释放后释放它。只需确保不会形成引用循环即可。
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?()
        }
    }
}

使用方法:

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

1
iOS 13破坏了我用来管理警报的辅助函数。
因为可能存在需要同时显示多个警报的情况(最近的在旧的上面),例如当您显示一个“是”或“否”的警报,同时您的Web服务返回错误时您通过警报显示它(这是一个极限情况,但它可能发生)。
我的解决方案是扩展UIAlertController,让它拥有自己的alertWindow以供呈现。
优点是当您关闭警报时,窗口会自动关闭,因为没有强引用留下,所以不需要进行进一步的修改。
免责声明:我刚刚实施了它,所以我仍然需要看看它是否一致...
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)
}

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接