ひ to り go と

ちょっとした NSView の使い方

MacDev

AppKit はとにかく資料が少ない。世の中に出回っている情報は古く、最新情報は OS X の新バージョンが出るタイミングで発行される AppKit Release Notes ぐらいにしか書かれていなかったりするので、これを読んでいないとすぐに置いていかれてしまう。

今日は NSView の描画の話。「UIView みたいに backgroundColor プロパティがないから面倒だ」みたいな話をときどき見かけるけど、正しい方法を知っていればそこまで手間はかからない。

highlighted プロパティに応じて YES ならレッド、NO ならグレーの角丸矩形を描画する NSView サブクラスを考える。

@property (nonatomic, getter=isHighlighted) BOOL highlighted;

方法 1:-drawRect: を使う描画

NSView のサブクラスで描画処理を実装する最もオーソドックスな方法。Layer Backed View でも使える。

#pragma mark - Properties
-(BOOL)isHighlighted { return _isHighlighted; } -(void)setHighlighted: (BOOL)isHighlighted { if (_isHighlighted != isHighlighted) { _isHighlighted = isHighlighted; [self setNeedsDisplay: YES]; } }
#pragma mark - Drawing
-(void)drawRect: (NSRect)dirtyRect { NSColor* backgroundColor = (_isHighlighted ? [NSColor redColor] : [NSColor grayColor]); [backgroundColor setFill]; NSBezierPath* bezierPath = [NSBezierPath bezierPathWithRoundedRect: self.bounds xRadius: 4.0f yRadius: 4.0f]; [bezierPath fill]; }

これで AppKit が適切なタイミングで描画してくれる。

ただし Layer Backed の場合はビューのサイズでビットマップを生成してそこに描画するので、今回みたいに単純な描画内容では無駄が多い。特に HiDPI ではメモリを大きく消費してしまうし、ユーザがウインドウを 1 px リサイズするたびにビットマップ作り直しになるような無駄な処理は避けたい。サイズ固定の小さなビューならほとんど気にすることはないけれども。

方法 2:Layer Backed View で CALayer のプロパティを使う描画

実のところ Layer Backed View なら UIView と同じようなことができる。CALayer の持つプロパティは OS X も iOS もほとんど同じであり、それらをセットするだけ。

[view.layer setBackgroundColor: [NSColor grayColor].CGColor];
[view.layer setCornerRadius: 4.0f];

今回は背景カラーを変えるだけだから -setHighlighted: の中で直接セットしてもいいけど、複雑な描画に備えて真っ当な方法を書いておく。

OS X 10.8 Mountain Lion で -updateLayer というメソッドが追加された。-wantsUpdateLayer で YES を返すようにするとこれが -drawRect: の代わりに呼ばれるようになる。

Layer Backed View + 方法 1 では -drawRect: を使って描画したビットマップを layer の contents にセットする、ということを AppKit が内部でやっていたのだけど、その代わりの動作をここに実装するのだ。

-(instancetype)initWithFrame: (NSRect)frame
{
    if (self = [super initWithFrame: frame]) {
        [self setWantsLayer: YES];
        [self setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawOnSetNeedsDisplay];
        [self.layer setCornerRadius: 4.0f];
    }
    return self;
}
-(BOOL)wantsUpdateLayer
{
    return YES;
}
#pragma mark - Properties
-(BOOL)isHighlighted { return _isHighlighted; } -(void)setHighlighted: (BOOL)highlighted { if (_isHighlighted != highlighted) { _isHighlighted = highlighted; [self setNeedsDisplay: YES]; } }
#pragma mark - Drawing
-(void)updateLayer { [super updateLayer]; NSColor* backgroundColor = (_isHighlighted ? [NSColor redColor] : [NSColor grayColor]); [self.layer setBackgroundColor: backgroundColor.CGColor]; }

このメソッドを使う大きな理由が layerContentsRedrawPolicy というプロパティの存在。AppKit が描画タイミングを管理してくれるのだ。ビューのサイズが変わったときに再描画するかどうかを決めたり、-setNeedsDisplay: の仕組みを使うことで無駄に何度も再描画するのを防いだりできる。

ちなみに、動的に見た目を変更しないのであればサブクラスなんて作らなくてもいい。

NSView* view = [[NSView alloc] initWithFrame: ~~];
[view setWantsLayer: YES];
[view.layer setBackgroundColor: [NSColor grayColor].CGColor];
[view.layer setCornerRadius: 4.0f];

Share

(参考になったらぜひ。記事を書くモチベーションの向上に役立てます。)