UIScreen でミラーリング

iPad2 ならホーム画面でもなんでも外部ディスプレイに出力できるんだけど、iPad はアプリ自体が対応しないと出力できない。今更なんだけど調べてみた。

使い方

お手頃なサンプルアプリを Apple が公開してくれてる。外部ディスプレイを接続したときの UIScreen 取得や接続の検出方法、外部ディスプレイの解像度の取得や指定方法なんかが分かりやすい。お勉強なら、これを読むのが良さそう。:-)


コードの基本形はこんな感じ。

- (void)viewDidLoad
{
    [super viewDidLoad];
    ...
    // external display
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenDidChange:) name:UIScreenDidConnectNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenDidChange:) name:UIScreenDidDisconnectNotification object:nil];
}

- (void)screenDidChange:(NSNotification *)notification {
    NSArray *screens;
    screens = [UIScreen screens];
    if ([screens count] > 1) {     // 外部ディスプレイあり
        self.extScreen = [screens objectAtIndex:1];
        NSArray *screenModes = [self.extScreen availableModes];
        self.extScreen.currentMode = [screenModes lastObject];

        self.extWindow = [[UIWindow alloc] initWithFrame:[self.extScreen bounds]];
        self.extWindow.screen = self.extScreen;
        UIView *view = [[UIView alloc] initWithFrame:[self.extWindow bounds]];
        view.backgroundColor = [UIColor blueColor];
        [self.extWindow addSubview:view];
        [view release];
        [self.extWindow makeKeyAndVisible];

    } else {    // 外部ディスプレイなし
        self.extScreen = nil;
        self.extWindow = nil;
    }
}


試してて思ったのは、メイン側とは独立したビューインスタンスを外部ディスプレイ用に作成してあげないとダメということ。たとえば、iPad で表示している要素を外部ディスプレイ側へ設定すると、iPad 側のビュー要素が消えて、ディスプレイ側に表示されるようになるので注意が必要。

        UIView *mainView = self.view;
        [view addSubview:[mainView.subviews objectAtIndex:1]]; 
        [self.extWindow addSubview:view];


あと、この方式だとiPad 側と外部ディスプレイ側のスクリーン上のビューを連動させるような仕掛けが必要になるので割とめんどい。^^;)


OSS 利用

簡単にiPad の表示要素をミラーリングしたいなら、画面のスナップショットをとって外部ディスプレイ側に設定するのが定石らしい。ちなみにこの方式の場合、こちら(↓)さんがコードを提供してくれてる。BSDライセンスなので使いやすいし。


Screen Mirroring on iPad を参考にしつつ、試してみるとこんな感じ。:-)

  1. QuartzCore.framework, CoreGraphics.framework のロードする。
  2. ViewController.m に "UIApplication+ScreenMirroring.h" をインポートする。
  3. ViewController.m の viewDidLoad に notification 設定する。
  4. ViewController.m に screenDidChange: を実装する。
- (void)viewDidLoad
{
    [super viewDidLoad];
    ...
    // external display
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenDidChange:) name:UIScreenDidConnectNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenDidChange:) name:UIScreenDidDisconnectNotification object:nil];
}

- (void)screenDidChange:(NSNotification *)notification {
    NSArray *screens;
    screens = [UIScreen screens];
    if ([screens count] > 1) {     // 外部ディスプレイあり
        [[UIApplication sharedApplication]  setupScreenMirroring];
    } else {    // 外部ディスプレイなし
        [[UIApplication sharedApplication] disableScreenMirroring];
    }
}


ただ、この実装で JPEG画像(32642448, 1.4MB)+ スクロールビューで試してみたら、スクロール時に iPad の画面も外部ディスプレイもカクカク動く。同じコードで外部使わないとなめらかに動く。iphoneos-screen-mirroring のソースみたら、内部で持ってるリフレッシュレートでメイン画面のスナップショットをとって、外部ディスプレイのイメージを差し替える実装になってるのが重いらしい。そりゃそーだ。^^;)

まぁ、簡単に組み込めるし、iPad での画面出力はおまけって割り切るなら、これを使うのが良いんだろね。:-)


おまけ

iphoneos-screen-mirroring に次の定義がある。

CGImageRef UIGetScreenImage(); // Not so private API anymore

「Not so private API anymore」ってなんでしょ?と思ってたら 琴線探査 さんが解説してくれてた。:-)


もともと Private API だったんだけど、いつの間にやら Apple が利用を認めた(黙認する?)っぽいとのこと。なので AppStore に公開するアプリでも使えるよ、って意味らしい。なるほどね。:-)