社内の別の開発チームにMacとRetinaなiPadを渡してアプリ作ってもらった結果、非Retina iPadで実行したらいろいろぼやけてしまってデザイナーまじおこ、などという事が起きてしまったため、その辺の傾向と対策を適当に書いてみようと思います。
(最初から非Retinaデバイスも渡しておけば済んだ話...とも言い切れず。)
なお、公式ドキュメント等は熟読していませんので、間違った事を言っていたりもっと楽な方法がありましたらご教示をお願いいたします。
直線
UIViewの派生クラスを作成して単純な図形や表などを描画する事があるかと思いますが、何の偏見も無く(3,3)-(10,3)の水平線を1ポイントで描こうと思ったら、最初はこんな感じのコードを書くのではないでしょうか。
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 1);
[[UIColor redColor] setStroke];
CGContextMoveToPoint(c, 3, 3);
CGContextAddLineToPoint(c, 10, 3);
CGContextStrokePath(c);
}
1ポイント(非Retina1ドット、Retina2ドット)の水平線が描かれるかと思いきや、非Retinaの方はぼやけてしまって期待した結果になりません。
「よくわからんけど、ぼやけるならアンチエイリアシング無効にしちゃえばいいじゃん」と考えがちですが、やってみると...
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetAllowsAntialiasing(c, false); // これ
CGContextSetLineWidth(c, 1);
[[UIColor redColor] setStroke];
CGContextMoveToPoint(c, 3, 3);
CGContextAddLineToPoint(c, 10, 3);
CGContextStrokePath(c);
}
ぼやけてはいませんが、非RetinaではY=3ではなくY=2として描画されてしまいます。
Retinaデバイスが登場する前はアンチエイリアシングを無効にしてY座標を+1するという手法も有りだったかもしれませんが、現在では非RetinaとRetinaで描画結果が異なる以上、+1案はNGです。
さて、ここからは私の勝手な憶測ですが、CoreGraphicsで指定している座標はドットの中央ではなく、格子のクロスしている点を指し示しているのだと思います。
つまり、非Retina環境において、以下のようなコードは、
CGContextMoveToPoint(c, 3, 3);
CGContextAddLineToPoint(c, 10, 3);
CGContextStrokePath(c);
CoreGraphicsの解釈では、理論上以下のように描画を指定した事になり、
各ドットへアンチエイリアシング込みでレンダリングした結果、以下のようになってしまうのだと思われます。
この仕様が正しければ、非Retinaで液晶1ドットに1ポイントの直線を奇麗に描きたいなら、以下のようにコーディングする必要があると思われます。
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetAllowsAntialiasing(c, false); // trueでも問題ない
CGContextSetLineWidth(c, 1);
[[UIColor redColor] setStroke];
CGContextMoveToPoint(c, 3, 3 + 0.5);
CGContextAddLineToPoint(c, 10, 3 + 0.5);
CGContextStrokePath(c);
}
結果、非RetinaでもRetinaでも期待した描画結果になります。
ちなみに角丸四角形のような図形はこうした0.5を足す引くなりして微調整するしかないと思いますが、単純な矩形や水平線・垂直線でしたら、UIRectFill
を使う方が楽かと思います(アルファブレンドを使う必要が有る場合はUIRectFillUsingBlendMode
を使います)。
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetAllowsAntialiasing(c, false);
[[UIColor redColor] setFill];
UIRectFill(CGRectMake(3, 3, 7, 1));
}
ばっちりですね。
個人的には変な足し算引き算が出ないUIRectFill
やUIRectFrame
などを使った方が精神衛生上良いのではないかと思います。
(なお、今回は動作速度に関しては一切考慮していません。)
画像 (centerプロパティの罠)
例えばタッチした位置に画像が追加されるプログラムを作ったとします。
(タッチした位置=画像の中央となるように配置することとします。)
表示する画像はこちら(15x15)。
(実際はRetina用の画像も別途用意されていると仮定します。)
#import "ViewController.h"
@implementation ViewController
- (void)loadView
{
[super loadView];
self.view.backgroundColor = [UIColor blueColor];
}
- (BOOL)prefersStatusBarHidden
{
return YES;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
UIImageView *v = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"a"]];
v.center = [t locationInView:self.view];
[self.view addSubview:v];
}
@end
UIViewのcenterプロパティを使ってサクッと組んでみました。
このcenterプロパティ、コードもスッキリ書けて便利なのでつい使ってしまうのですが、非Retina機で実行すると以下のようになります。
(12倍に拡大しています。)
完全にぼやけちゃっていますね。
それもそのはず、centerプロパティはUIViewの中央の座標を指定して座標を変更するため、今回のUIImageViewのような(15x15)といった奇数サイズのビューでこのプロパティを使ってしまうとframe = {{10.5, 7.5}, {15, 15}}
というようにframeのoriginが半端な数になってしまい、結果的に画像がぼやけて表示されてしまいます(Retinaだとぼやけないので気がつきにくいです)。
回避策としましては、centerプロパティをどうしても使いたくてたまらなかったり、アフィン変換でビューを90度回転させたりするような場合は、あらかじめ画像のサイズを偶数にしておくと楽だと思います。
そうでない場合は、面倒くさいですが、frameのoriginが半端にならないように補正するしかないと思います。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *t = [touches anyObject];
UIImageView *v = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"a"]];
v.center = [t locationInView:self.view];
// ぼやけるのを防止するいい加減な補正コード
CGRect rc = v.frame;
rc.origin.x = (int)rc.origin.x;
rc.origin.y = (int)rc.origin.y;
v.frame = rc;
[self.view addSubview:v];
}
今回はUIImageViewで例を挙げましたが、UILabelやUIButtonなど、各種コントロールでも同様の症状が起きますのでご注意ください。
開発の規模によっては、UIViewのカテゴリでcenterForUI
などと言った名前のプロパティを自作してframe.originが半端にならないようにするというのも手かもしれませんね。
@interface UIView (HogeHoge)
@property (nonatomic) CGPoint centerForUI;
@end
@implementation UIView (HogeHoge)
- (CGPoint)centerForUI
{
return self.center;
}
- (void)setCenterForUI:(CGPoint)centerForUI
{
// アフィン変換などをしているとたぶんNGです。
CGSize size = self.bounds.size;
self.frame = CGRectMake((int)(centerForUI.x - size.width / 2), (int)(centerForUI.y - size.height / 2), size.width, size.height);
}
@end
// 使用例
{
v.centerForUI = [t locationInView:self.view];
}
ただし、当然ながら自分のUIViewの座標が正しくても、乗っている親ビューの座標が半端だったり拡大縮小していたりする場合、やっぱりぼやけますので別途対処が必要です。
拡大縮小はあまり良いアイディアはありませんが、ビューの階層ぐらいでしたらUIWindow座標系などへ一度変換してそこでoriginを補正、再度親ビュー座標系へ変換すれば、ぼやけるのを回避できるのではないかと思います。
なお、CGRectIntegral
という関数を使うと、CGRectを整数にしてくれて便利そうに見えますが、こちらの関数は指定したCGRectが完全に収まるような(整数で構成された)CGRectを返す仕様になっているため、場合によってsizeが大きくなる事があります。UIImageViewでそれをやられてしまうと画像とビューでサイズが合わずにぼやける...などという事になりますのでご注意ください。