iOS:最前面に画面を出す/最前面の画面を知る


iOSアプリにおける、最前面のUIViewControllerについてまとめます。

※最新（2022年2月）のソースコードはこちらに記載しています。
https://www.fuwamaki.com/article/258

## 最前面のUIViewControllerは2種類あった

1. `UIApplication.shared.keyWindow?.rootViewController` 
2. `UIApplication.shared.delegate?.window??.rootViewController`

の2種類、最前面のUIViewControllerを取得する方法があり、
これらを用いる際に区別するためにはどうすればいいか、
を考えたことがこの記事を書くことにした発端です。

他の方法が実はあるなど、知っている方がいればご教示ください。
また、以下自分の解釈が結構あるので、間違いあればご教示ください。

## UIApplicationのkeyWindow

UIApplication.shared.keyWindowのref
https://developer.apple.com/documentation/uikit/uiapplication/1622924-keywindow

> This property holds the UIWindow object in the windows array that is most recently sent the makeKeyAndVisible() message.

**一番最近表示したWindow** と自分は解釈しました。

その場合、keyWindow.rootViewControllerは
**一番最近表示したUIViewController** となります。

※ [makiKeyAndVisibleのref](https://developer.apple.com/documentation/uikit/uiwindow/1621601-makekeyandvisible)

## UIApplication DelegateにあるWindow

UIApplication.shared.delegate?.windowのref
https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623056-window

> This property contains the window used to present the app’s visual content on the device’s main screen.
> Implementation of this property is required if your app’s Info.plist file contains the UIMainStoryboardFile key. Fortunately, the Xcode project templates usually include a synthesized declaration of the property automatically for the app delegate. The default value of this synthesized property is nil, which causes the app to create a generic UIWindow object and assign it to the property. If you want to provide a custom window for your app, you must implement the getter method of this property and use it to create and return your custom window.

**Viewをデバイスの最前面に表示するためのWindow** と自分は解釈しました。

このWindowの利用は、MainStoryboardをInfo.plistに
設定することが前提となっているようです。

## 使い分けまとめ

最前面のUIViewControllerの利用は主に、

- 最前面の画面を知りたい
- 最前面に画面を出したい

の2つだと思います。これを元に表にするとこんな感じ。

| | 最前面の画面を知りたい |  最前面に画面を出したい |
| :-- | :--: | :--: |
| keyWindowTopViewController | ◎ | ○ |
| topViewController | × | ◎ |

※keyWindowTopViewController...`UIApplication.shared.keyWindow?.rootViewController`
※topViewController...`UIApplication.shared.delegate?.window??.rootViewController`

正直、keyWindowTopViewControllerだけで両方の役割を果たせます。
しかし、下記の役割にする方が確実かと、自分は検証して思いました。

- 最前面の画面が知りたければkeyWindowTopViewControllerを使う
- 最前面に画面を出したければtopViewControllerを使う

## keyWindowTopViewControllerで最前面に画面を出して不整合が出るケース

keyWindowTopViewControllerにはWindowを設定できます。
そのWindowの上に別の画面を出したい場合、
`keyWindowTopViewController.present` で画面を表示する場合、
下記のような不整合が発生します。

![](https://i.imgur.com/oHLjkXj.png)

左側が今回の対象ケース、右側がtopViewControllerを用いたケースの
rootViewControllerを出力した結果です。

左側はほとんどはnil, viewだけ何故か取得できていることになります。
これはどうゆう状況かというと、

**表示したい画面は表示されたけど、何も操作できない** という状態です。

必ずしもこういうケースになるとは限らないと思いますが、
keyWindowTopViewControllerで最前面に画面を表示するのは
避けたほうが良いと思いました。

## 結論を元にしたextension

以上のことを考慮し、便利であろうextensionを考えてみました。
どちらも最前面をたどる実装にしています。

```swift
import UIKit

extension UIApplication {
    // 最前面の画面を知るために用いる。
    class func keyWindowTopViewController(on controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        if let navigationController = controller as? UINavigationController {
            return keyWindowTopViewController(on: navigationController.visibleViewController)
        }
        if let tabController = controller as? UITabBarController,
            let selected = tabController.selectedViewController {
            return keyWindowTopViewController(on: selected)
        }
        if let presented = controller?.presentedViewController {
            return keyWindowTopViewController(on: presented)
        }
        return controller
    }

    // 最前面に画面を表示するために用いる。
    class func topViewController() -> UIViewController? {
        guard let rootViewController = UIApplication.shared.delegate?.window??.rootViewController else { return nil }
        var presentedViewController = rootViewController.presentedViewController
        if presentedViewController == nil {
            return rootViewController
        } else {
            while presentedViewController?.presentedViewController != nil {
                presentedViewController = presentedViewController?.presentedViewController
            }
            return presentedViewController
        }
    }
}
```

使い方例:

```swift
if UIApplication.keyWindowTopViewController() is HogeViewController {
    //処理
    return
}
```

```swift
UIApplication.topViewController()?.present(hogeViewController, animated: true, completion: nil)
```

## まとめ

「いや、〇〇という例外があるので違う気がします」など指摘あればご教示ください。
ぜひ他の人にも↑使ってもらって試して頂ければと思います。

## 参考

- https://qiita.com/Simmon/items/80dbf50bc85c527f10f3
- https://qiita.com/maebaru/items/ae954b19ad334f990538