iOS アプリ開発に関連するトピックを提供しています。

アプリの画面を高解像度でミラーリングする HiResMirroring

2018 年モデルの iPad Pro 10 inch / 12 inch では HDMI ケーブルを使用して 4K ディスプレイと接続することで、従来よりも解像度の高い外部ディスプレイ出力が使用可能です。しかし、アプリ側で外部ディスプレイ出力用の処理を実装していない場合、ケーブルで接続しても iPad の画面がそのままミラーリングされます。iPad 側の解像度を基準として、外部ディスプレイに対してミラーリングが行われるため、高解像度を十分に活用することができません。

これとは逆に、外部ディスプレイ側の解像度を基準として、iPad に対してミラーリングすることができれば高解像度を余すことなく活用することができます。このような処理を実装したコードをフレームワークとして公開しました。

github.com

通常のミラーリング

アプリ側で外部ディスプレイ出力用の実装をしない場合のミラーリング表示は以下のようになります。

https://github.com/watanabetoshinori/HiResMirroring/raw/master/Preview/MirroringDefault.png

iPad アプリでは 4K サイズの猫画像を表示していますが、全体を表示できていません。

高解像度ミラーリング

HiResMirroring を使用した場合のミラーリング表示は以下のようになります。

https://github.com/watanabetoshinori/HiResMirroring/raw/master/Preview/MirroringHi-Res.png

外部ディスプレイ側の解像度を基準とするため、4K サイズの画像も全体が表示されます。 また、通常のミラーリングと同様に iPad 画面を操作することで外部ディスプレイの画面を操作することができます。

実装

外部ディスプレイの UIWindow に対して iPad 画面の UIWindow が持っていた rootViewContoller を渡し、iPad 画面にはミラーリング操作用の ViewController を新たに設定することで擬似的なミラーリングを再現しています。

NotificationCenter.default.addObserver(forName: UIScreen.didConnectNotification, object: nil, queue: nil) { (notification) in
    let newScreen = notification.object as! UIScreen
    let screenDimensions = newScreen.bounds
    
    let newWindow = UIWindow(frame: screenDimensions)
    newWindow.screen = newScreen

    // Set current rootViewController to the External Screen.
    newWindow.rootViewController = self.rootViewController
    
    // Set mirroring viewController to Main Screen.
    let controller = MirroringViewController.instantiate(with: newWindow)
    self.rootViewController = controller

    ...
}

ミラーリング操作用の ViewController では外部ディスプレイのスナップショットを取得して表示しています。

let snapshotView = window.screen.snapshotView(afterScreenUpdates: false)

また、iPad 画面の UIWindow に対するタッチイベントを捕まえ、外部ディスプレイの UIWindow に対して再送することでミラーリングでの操作を実現しています。タッチイベントの再送には FakeTouch を使用しています。

func handleSendEvent(_ event: UIEvent) {
    guard let touches = event.allTouches, touches.isEmpty == false else {
        return
    }

    let touchObjects = touches.map({ touchObject(for: $0) })
    
    UIEvent.send(touches: touchObjects)
}