Xcode11で外部ディスプレイに表示できるようにする方法(iOS13対応)

# はじめに
iPhone/iPadと[Apple Lightning - Digital AVアダプタ](https://amzn.to/2VWVXHn)を用いていることでHDMIモニター（TV）に映像を出力することができます。
通常はミラーリングとなりますが、本方法を用いると液晶(OLED)とHDMIで別の表示することができます。
例えば、iPhoneの液晶(OLED)を操作画面として、HDMIを表示画面にするなどできます。

Xcode11で外部ディスプレイに表示する方法を試行錯誤したので投稿します。
<del>まだわかっていないところがあり、コメントいただければと思います。</del>→解決しました。

# 実装
いくつかのサイトを参考に実装しました。
最後に参考サイトのリンクしますので確認してみてください。
また、[サンプルプロジェクトをgithubに公開](https://github.com/FromF/SwiftExternalDisplay/)していますので参考にしてください。

**[2019.10.23追記]**
iOS13からのUISceneで外部ディスプレイに出力できる方法を@ura14hさんのコメントをいただいてできるようになりました。
iOS12までとiOS13以降と２つにわけました。

## iOS13以降の方法
UISceneを利用し接続切断通知を受け取る方法を記載します。

### 外部ディスプレイの接続切断通知を受け取れるように設定
接続切断通知を受け取れるようにします。

```swift:通知設定
    private func setupNotification() {
        let center = NotificationCenter.default
        
        center.addObserver(self, selector: #selector(externalScreenWillConnect(notification:)), name: UIScene.willConnectNotification, object: nil)

        center.addObserver(self, selector: #selector(externalScreenDidDisconnect(notification:)), name: UIScene.didDisconnectNotification, object: nil)

    }
```

### 接続処理を実装
Storyboardで作成した画面に**StoryboardID**を設定しておき接続時に画面を生成します。
必要に応じて外部から呼び出せるようにインスタンスを保持しておくと便利だと思います。

```swift:接続処理
    /// 外部ディスプレイを接続されたときに処理する
    /// - Parameter notification:接続されたUIWindowSceneインスタンス
    @objc func externalScreenWillConnect(notification: NSNotification) {
        guard let windowScene = notification.object as? UIWindowScene else { return }
        guard externalWindow == nil else { return }
        guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: "ExternalDisplayViewController") as? ExternalDisplayViewController else { return }

        externalWindow = UIWindow(frame: windowScene.screen.bounds)
        
        guard let _externalWindow = externalWindow else { return }
        _externalWindow.rootViewController = viewController
        _externalWindow.windowScene = windowScene
        _externalWindow.isHidden = false
        
        //外部ディスプレイのViewControllerをあとで操作できるようにインスタンスを保持する
        externalDisplayViewController = viewController
    }
```

### 切断処理を実装
外部ディスプレイが切断時の処理も記述します。

```swift:切断処理
    /// 外部ディスプレイを切断されたときに処理する
    /// - Parameter notification: 切断されたUIWindowSceneインスタンス
    @objc func externalScreenDidDisconnect(notification: NSNotification) {
        guard let windowScene = notification.object as? UIWindowScene else { return }
        guard let _externalWindow = externalWindow else { return }
        guard let _externalWindowScene = _externalWindow.windowScene else { return }
        
        //切断通知されたscreenと現在表示中のscreenが同じかチェックする
        if windowScene == _externalWindowScene {
            _externalWindow.isHidden = true
            externalWindow = nil
        }
    }
```



## iOS12までの方法
Xcode11ではUISceneがデフォルトになっています。iOS12までのUIScreenを利用できるように修正した後に、UIScreenから接続切断通知を受け取り処理をします。

### UISceneを無効化
Xcode11からUIScreenからUISceneに変わっています。しかしながら新しいUISceneで外部ディスプレイ対応する方法がわかりませんでした😢
なのでUISceneを無効化します。

[githubのサンプルプロジェクトのcommitログ](https://github.com/FromF/SwiftExternalDisplay/commit/3b38e91b666a254502d91c0d1e02d2f5aa296837)を参考にしてください。
概要は下記の通りです。
* `AppDelegate.swift`のUISceneに関連するメソッドを無効化（削除でも可) 
* `SceneDelegate.swift`の全てのメソッドを無効化（削除でも可)
* `AppDelegate.swift`に`var window: UIWindow?`を追加
* `Info.plist`のUISceneに関する記述を削除

### 外部ディスプレイの接続切断通知を受け取れるように設定
接続切断通知を受け取れるようにします。

```swift:通知設定
    /// 外部ディスプレイの接続と切断の通知を受け取れるようにする
    private func setupNotification() {
        let center = NotificationCenter.default
        
        center.addObserver(self, selector: #selector(externalScreenDidConnect(notification:)), name: UIScreen.didConnectNotification, object: nil)

        center.addObserver(self, selector: #selector(externalScreenDidDisconnect(notification:)), name: UIScreen.didDisconnectNotification, object: nil)
    }
```

### 接続処理を実装
Storyboardで作成した画面に**StoryboardID**を設定しておき接続時に画面を生成します。
必要に応じて外部から呼び出せるようにインスタンスを保持しておくと便利だと思います。

```swift:接続処理
    /// 外部ディスプレイを接続されたときに処理する
    /// - Parameter notification:接続されたUIScreenインスタンス
    @objc func externalScreenDidConnect(notification: NSNotification) {
        guard let screen = notification.object as? UIScreen else { return }
        guard externalWindow == nil else { return }
        guard let viewController = self.storyboard?.instantiateViewController(withIdentifier: "ExternalDisplayViewController") as? ExternalDisplayViewController else { return }

        externalWindow = UIWindow(frame: screen.bounds)
        externalWindow?.rootViewController = viewController
        //iOS13.0からDeprecatedになっているがUISceneを無効化しているのでこちらのプロパティーを設定する
        externalWindow?.screen = screen
        externalWindow?.isHidden = false
        
        //外部ディスプレイのViewControllerをあとで操作できるようにインスタンスを保持する
        externalDisplayViewController = viewController
    }
```

### 切断処理を実装
外部ディスプレイが切断時の処理も記述します。

```swift:切断処理
    /// 外部ディスプレイを切断されたときに処理する
    /// - Parameter notification: 切断されたUIScreenインスタンス
    @objc func externalScreenDidDisconnect(notification: NSNotification) {
        guard let screen = notification.object as? UIScreen else { return }
        guard let _externalWindow = externalWindow else { return }
        
        //切断通知されたscreenと現在表示中のscreenが同じかチェックする
        if screen == _externalWindow.screen {
            _externalWindow.isHidden = true
            externalWindow = nil
            //外部ディスプレイのViewControllerを削除する
            externalDisplayViewController = nil
        }
    }
```

# 最後に
<del>他の記事を参考に作成しましたが、`externalWindow?.screen`すると**Deprecated**になっているプロパティで警告されてしまいます。</del>
<del>正しい実装方法を調べてみましたが、わかりませんでした。</del>
<del>コメントをいただければと思います。</del>
<del>（あわせてこの記事とサンプルプロジェクトを変更していきたいと思います。）</del>
@ura14h さんのコメントによりUISceneを使った方法がわかりました。
本当にありがとうざいました。

## 参考サイト
* [How To Display Custom Content On A External Screen From A iOS Device](http://www.spazstik-software.com/blog/article/how-to-display-custom-content-on-a-external-screen-from-a-ios-device)
* [Supporting External Displays](https://www.swiftjectivec.com/supporting-external-displays/)
* [【swift】iPhoneから外部ディスプレイにViewを表示【iOS】](https://qiita.com/beckyJPN/items/c002dc7aaac0295a30ba)
* [サブディスプレイをキャンバスとして使うiPad用お絵描きアプリ](https://qiita.com/retsaba/items/078407ff2676fa6fa3b8)