24/7 twenty-four seven

iOS/OS X application programing topics.

アプリケーションを iPhone 4 の Retina Display に対応するための方法いろいろ

iPhone 4 の Retina Display の高解像度表示にアプリケーションを対応させるための方法をいくつか書きます。
これだけですべての場面に対応できるわけではないですが(例えば OpenGL での描画など)何かの役に立てばと思います。

高解像度の画像リソースを用意する

Retina Display は従来のディスプレイの倍の解像度を持っているので、倍の解像度に合わせた画像を用意します。
もちろん単純に拡大しただけではダメなので、解像度が高くなったぶん、なめらかな画像を用意することになります。



上記の例は、上が従来の画像、下が Retina Display 対応の画像です。

命名規則によって自動的に解像度に合わせた画像を読み分ける

[UIImage ImageNamed:] で読み込む場合は、ファイル名のサフィックスを判断して自動的にディスプレイの解像度に合わせた画像を読み分けてくれます。
高解像度の画像ファイル名には "@2x" のサフィックスを付けます。
上記の例だと、それぞれ

time_0.png
time_0@2x.png

という名前を付けます。
そのため [UIImage imageNamed:] を使っている部分は画像リソースを追加するだけで Retina Display に対応できます。
コードを変更する必要はありません。


ちなみに、iOS 4 から、imageNamed: の引数は拡張子が必要なくなりました。

Special Considerations
On iOS 4 and later, the name of the file is not required to specify the filename extension. Prior to iOS 4, you must specify the filename extension.

UIImage Class Reference

しかし、iPhone OS 3.x で実行された場合は、拡張子を付けた名前を指定しないと画像が読み込まれないため、動作環境を iOS 4 以上に限定しない限り、従来通り拡張子を含んだ形で指定することになります。


imageNamed: 以外のメソッドで画像を読み込んでいる場合は、上記の機構は働きません。
ドキュメントには imageWithContentsOfFile: と、initWithContentsOfFile: でも同様だと書いてあるのですが、試してみたところダメでした。

On devices with high-resolution screens, the imageNamed:, imageWithContentsOfFile:, and initWithContentsOfFile: methods automatically looks for a version of the requested image with the @2x modifier in its name. It if finds one, it loads that image instead. If you do not provide a high-resolution version of a given image, the image object still loads a standard-resolution image (if one exists) and scales it during drawing.

Drawing and Printing Guide for iOS: Supporting High-Resolution Screens

解像度に応じて処理を分岐して、読み込む画像を変化させる

imageNamed: は読み込んだ画像を自動的にキャッシュするため、使用したくない場合があります。
その場合、imageWithContentsOfFile: か initWithContentsOfFile: を使用することになりますが、これらのメソッドは上記の命名規則による読み分けの機構が働かないため、自分でディスプレイの解像度に合わせて、読み込む画像を変える必要があります。

例えば、下記のようにします。

static BOOL isPad;

NSString* model = [[UIDevice currentDevice] model];
isPad = [model rangeOfString:@"iPad"].location != NSNotFound;

if (!isPad && [UIScreen instancesRespondToSelector:@selector(scale)] && [[UIScreen mainScreen] scale] == 2.0) {
    NSString *path2x = [[mainBundle bundlePath] stringByAppendingPathComponent:
                        [NSString stringWithFormat:@"%@@2x.%@", [name stringByDeletingPathExtension], [name pathExtension]]];
    return [UIImage imageWithContentsOfFile:path2x];
}


UIScreen の scale プロパティの値を見ることで、座標の 1 ポイントとディスプレイの 1 ピクセルがどのような比率で対応するかが分かります。
要するに Retina Display だと倍の密度があるので 2.0 になります。従来のディスプレイでは 1.0 です。scale の値は 0 にはなりません。


ただし、iPad で iPhone アプリケーションを 2 倍モードで実行した場合も、scale の値は 2.0 になります。
(scale プロパティはいちおう iOS 4.0 以降ですが、iPhone OS 3.2 から存在しているので、respondToSelector:@selector(scale) は YES になりますし、値も返ってきます。)
そのため、モデル名を調べて、iPad かどうかを判断しています。
ちなみに、シミュレータで実行した場合はモデル名が必ず "iPhone Simulator" になる (iPad シミュレータでも) ので、上記のコードだと正しく動きません。
「必ず」ではありませんでした。iPhone アプリケーションを iPad のシミュレータの iPhone モードで実行した場合に "iPhone Simulator" と返ってきます。
iPad アプリをシミュレータで実行した場合は "iPad Simulator" になります。

ビットマップイメージをグラフィックコンテキスト (CGContextRef) に新しく描画する場合

画像をオフスクリーンで合成するなど、グラフィックコンテキストにビットマップを描画する場合は、解像度に合わせてグラフィックコンテキストを作る必要があります。


解像度に合わせてビットマップイメージ用のグラフィックコンテキストを作成するには UIGraphicsBeginImageContextWithOptions 関数を使用します。

void UIGraphicsBeginImageContextWithOptions(
   CGSize size,
   BOOL opaque,
   CGFloat scale
);


iPhone OS 3.x までの環境で使用していた、UIGraphicsBeginImageContext 関数に opaque と scale の引数が追加された新しい関数です。

void UIGraphicsBeginImageContext (
   CGSize size
);


今回は UIImage のカテゴリとして下記のようなメソッドを考えます。

@implementation UIImage()

- (UIImage *)imageByCompositeWithImage:(UIImage *)image {
    CGRect rect = CGRectMake(0.0f, 0.0f, self.size.width, self.size.height);
    if (UIGraphicsBeginImageContextWithOptions != NULL) {
        UIGraphicsBeginImageContextWithOptions(rect.size, NO, self.scale);
    } else {
        UIGraphicsBeginImageContext(rect.size);
    }
    [self drawInRect:rect];
    [image drawInRect:rect blendMode:kCGBlendModeNormal alpha:1.0f];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}

@end


まず OS バージョンを UIGraphicsBeginImageContextWithOptions 関数のポインタが NULL を指しているかどうかで判断しています。
UIGraphicsBeginImageContextWithOptions 関数が利用できる場合は iOS 4 以上かつ iPhone 4 である可能性があるので、scale 引数を渡し、解像度に合わせたグラフィックコンテキストを開始します。

グラフィックコンテキストを開始した以降の処理はこれまでと同様です。


今回の例では scale の値は UIImage のプロパティから取得しています。

UIGraphicsBeginImageContextWithOptions 関数および UIImage の scale プロパティは iOS 4 以降にしか存在しないため、この例では iPad かどうかは判断していません。


また、グラフィックコンテキストの仕様が iPhone OS 3.2 から下記のように変更になっているのでこちらも覚えておくといいと思います。

You use this function to configure the drawing environment for rendering into a bitmap. The format for the bitmap is as follows:

  • For bitmaps created in iPhone OS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data. If the opaque parameter is YES, the bitmap’s alpha channel is ignored.
  • For bitmaps created in iPhone OS 3.1.x and earlier, the drawing environment uses the premultiplied RGBA format to store the bitmap data.
iOS Developer Library

CATiledLayer にイメージを描画する場合

CATiledLayer を使用する場合、Retina Display だと tileSize に対する描画エリアの倍率がおかしくなります。
説明が難しいので以下の画像を見てください。こんなふうになります。笑えますね。


この問題を解決するには CATiledLayer を使用しているビューの contentScaleFactor の値を 1.0 に設定します。
設定するタイミングと場所は ビューの layoutSubviews メソッド内がいいでしょう。

@implementation TiledView

+ (Class)layerClass {
    return [CATiledLayer class];  
}

- (void)layoutSubviews {
    if ([self respondsToSelector:@selector(contentScaleFactor)]) {
        self.contentScaleFactor = 1.0f;
    }
}

@end

ビューが再表示される場合は layoutSubviews を再度呼ぶ必要があるかもしれません。
その場合は、ビューコントローラの viewWillAppear: で setNeedsLayout メソッドを呼びましょう。

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [tiledView setNeedsLayout];
}


TiledLayerView と「そのまま使える iPhone アプリプログラム」のサンプルコードは、この修正を施したバージョンになっていますのでご覧ください。

参考サイトなど

iOS Developer Library

まずは iPhone アプリケーションプログラミングガイドの該当の章を読みましょう。

WWDC 2010 Session Videos - Apple Developer Session 134 - Optimize your iPhone App for the Retina Display

WWDC のセッションビデオはデベロッパには無料で公開されていますので見ておきましょう。
Retina Display 対応のセッションは 134 番のセッションになります。