【Swift5】フォアグラウンドに復帰したら画面を表示する

作成したアプリにロック機能を実装した後、アプリがバックグラウンドからフォアグラウンドに復帰した時にもロック画面を表示したいなーと思い、調べてました。
復帰したら applicationDidBecomeActive(_:) でUI更新しましょうって書かれていることが多いのですが、新しく画面を出すと何も表示されないので ( ͡° ʖ̯ ͡°) ってなりました。
そんな人に向けた記事ですー。

前提条件

・Xcode12.2
・Swift5(Unused SwiftUI)

ダメだったコード

最初にコード載っけます。

NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification,
                                       object: nil,
                                       queue: nil) {
    DispatchQueue.main.async {
        let vc = ... // フォアグラウンド復帰時に表示したい画面
        self.present(vc, animated: true, completion: nil)
    }
}

これはダメでした。
うんともすんとも言いません ( ˙-˙ )
NotificationCenter を使っているのが原因ではなく、applicationDidBecomeActive(_:) でも同じです。

コンソールには whose view is not in the window hierarchy 的なエラーが出ましたー
iOSエンジニアは一度は遭遇したことあるエラーだと思いますが、ご存知の通り「画面ねーのにどうやって遷移すんの?w」って笑われてます。

applicationDidBecomeActive(_:) のドキュメントを見ると、

If your app was previously in the background, you can also use it to refresh your app’s user interface.
After calling this method, UIKit posts a didBecomeActiveNotification to give interested objects a chance to respond to the transition.

アプリが以前にバックグラウンドにあった場合は、それを使用してアプリの UI を更新することもできます。
このメソッドを呼び出して didBecomeActiveNotification を投げて、関心のあるオブジェクトに遷移に応答する機会を与えます。

的なことを言われてますが、だからやってんじゃんよ!って悪態つきたくなります。

動くコード

さきほどの画面がないってのは、遷移先の画面ではなく、遷移元(表示中)の画面のことです。
僕がこのエラーに遭遇したアプリの構成は、大元の ViewController に ContainerView があり、そこに UITabBarController と ViewController があり…とちょっと複雑な作りなんです。

最前面の ViewController を取得するコードを実装することも考えたのですが、それよりも UIWindow を新たに作ってどこからでも安全に呼び出せる方がいいなと思ったので、その方向で実装します。

どこからでも新規 UIWindow で呼び出せるクラスを作る

import UIKit

class ForefrontModal: NSObject {

    static let shared = ForefrontModal()  // 使う先々でクラス保持するのが嫌ってだけの理由です。
    private var window: UIWindow?
    private let rootVC: UIViewController = {
        let vc = UIViewController()
        vc.view.backgroundColor = .clear
        return vc
    }()

}

extension ForefrontModal {

    func present(vc: UIViewController, animate: Bool, completion: (() -> ())?) {
        vc.presentationController?.delegate = self  // dismiss を検知するためのデリゲート

        self.window = UIWindow()
        self.window?.backgroundColor = .clear
        self.window?.windowLevel = .statusBar
        self.window?.rootViewController = self.rootVC
        self.window?.makeKeyAndVisible()
        self.window?.rootViewController?.present(vc, animated: true, completion: completion)
    }

}

//MARK: - UIAdaptivePresentationControllerDelegate

extension ForefrontModal: UIAdaptivePresentationControllerDelegate {

    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        self.window?.isHidden = true
        self.window = nil
    }

}

遷移先の dismiss を検知する

遷移先の dismiss を遷移元に検知させるために、UIAdaptivePresentationControllerDelegate を使用します。
遷移先の dissmiss に以下のコードを追加します。

self.dismiss(animated: true, completion: nil)
guard let presentationController = self?.presentationController else {
    return
}
presentationController.delegate?.presentationControllerDidDismiss?(presentationController)

//----- dismiss が始まったときに検知するなら上、完了したときなら下を使う -----//

self.dismiss(animated: true, completion: {
    guard let presentationController = self?.presentationController else {
        return
    }
    presentationController.delegate?.presentationControllerDidDismiss?(presentationController)
})

これで検知することができます。

実際に呼び出す

アプリがバックグラウンドからフォアグラウンドに復帰した時にもロック画面を表示するためにやっていたので、最初のダメだったコードを修正します。

NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification,
                                       object: nil,
                                       queue: nil) {
    DispatchQueue.main.async {
        let vc = ... // フォアグラウンド復帰時に表示したい画面
        ForefrontModal.shared.present(vc: vc, animate: true, completion: nil)
    }
}

これで安全にロック画面が表示されます。
遷移元とは完全に独立していて didAppear らへんも無関係なので、安全に使えます ヽ(ヅ)ノ

さいごに

SwiftUI だとまた違う方法になりますが、出来ることがもっと増えてから取り組みたいと思います。

Swift
スポンサーリンク

コメント