[Mac] トラックパッド 〜 タッチイベントサンプル LightTable のソースを読む

2011年1月31日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

タッチイベントを扱うサンプルプログラムが Appleから提供されている。
LightTable

2本指でタッチした時のイベント処理を参考にする為に上記ソースを読んでみたところイベントの処理で面白いコードがあったので紹介する。


LightTable


LightTable を立ち上げると白いウィンドウが起動する。ここへ画像をD&Dするとその画像をジェスチャー操作で動かすことができるようになる。

画像は2本指ドラッグで移動、ピンチで拡大縮小操作ができる。3本指でスワイプすると左側の設定エリアの表示を切り替えられる。


ジェスチャーを扱う


ピンチやスワイプ、回転といったジェスチャーはあらかじめ NSResponder にメソッドが用意されているので簡単に利用できる。これは前回までで紹介した。一方、NSResponderに用意されていない、2本指のドラッグ操作などは自前で複数のタッチイベントを処理する必要がある。こういったマルチタッチ操作を意味のあるジェスチャーとして扱うには個々のタッチイベントの状態を管理して、一連の操作がジェスチャーだとみなす為の条件をチェックを行う必要があるので複雑になりがちである。例えば2本指操作の場合、最初に1本目のタッチ、次に2本目のタッチ、続いて両方の指が一定以上移動したかどうかチェック、そこで初めて2本指ドラッグのジェスチャーとみなされる。このあたりの処理を LightTable は実にうまく対処している。

以下に LightTable のクラス図をまとめてみた。

タッチ系のイベントは NSResponder で受ける。通常はそのサブクラスである NSView なり NSWindow でこれを処理することになる。サンプルプログラム LightTable の場合もメインのビューでイベントを受け取っているのだが、実際の処理は NSResponder のサブクラスである InputTracker へ移譲して行わせている。

ユーザからのタッチイベントは最初に LTView に届けられる(図内①)。LTView はこれを InputTracker のサブクラス(ClickTracker, DragTracker, DualTouchTracker)のインスタンスへ転送する(図内②)。InputTracker は NSMutableArray* _inputTrackers に格納されているのでイベント受信時には配列内の InputTracker の当該メソッドをまとめて呼び出している。
@implementation LTView

- (void)mouseDown:(NSEvent *)event {
    [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event];
}

- (void)mouseDragged:(NSEvent *)event {
    [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event];
}

- (void)mouseUp:(NSEvent *)event {
    [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event];
}

- (void)touchesBeganWithEvent:(NSEvent *)event {
    [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event];
}

- (void)touchesMovedWithEvent:(NSEvent *)event {
    [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event];
}

- (void)touchesEndedWithEvent:(NSEvent *)event {
    [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event];
}

- (void)touchesCancelledWithEvent:(NSEvent *)event {
    [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event];
}
上記コードは InputTrackerのインスタンスに _cmd 、すなわち現在処理中のメソッド名と同じ名前のセレクタをメッセージとして送る。例えば mouseDown: 内の [_inputTrackers makeObjectsPerformSelector:_cmd withObject:event] は配列内すべてのインスタンスに対して [(InputTrackerインスタンス) mouseDown:event] を実行するのと等価。

InputTracker の各サブクラス(のインスタンス)はそれぞれタッチイベントの状態を管理していて、一連のタッチイベントによってジェスチャーが構成されたタイミングで LTView の所定のメソッドをコールバックする(図内③)。例えば2本指のジェスチャーを管理している DualTouchTracker では touchesBeganWithEvent:、 touchesMovedWithEvent: の順番でイベントを受け取り、2本指でなおかつ移動量が一定のしきい値(threshold)を越えたタイミングで LTView の dualTouchesBegan: をコールバックしている。コールバックメソッドはプロパティの (SEL)beginTrackingAction で管理している。こうすることで低レベルなタッチイベントの管理は DualTouchTrackerに隠蔽され、LTView 側は目的の2本指によるジェスチャー(この場合はドラッグ)のみ必要な時にコールバックを受け取って処理を行うことができる。

このコードが優れている点はなんといっても一連のタッチイベントで構成されるジェスチャーという複雑な処理を InputTracker というクラスで隠蔽できるところ。利用側はジェスチャーが構成されたタイミングでコールバックを受けるだけでいい。さらに InputTracker は派生クラスを作ることで様々なジェスチャーに対応することができる。
また InputTracker の管理の仕方もよく出来ている。使う側は、あらかじめ使いたいジェスチャーに対応する InputTrackerの派生クラスのインスタンスを NSMutableArray へ詰めておき、NSResponder のタッチイベントを受け取ったらそれを配列内の複数の InputTracker へ転送するだけで良い。今後対応したいジェスチャーが増えた時は、必要なインスタンスを配列へ詰めておくだけで良い。一種のプラグイン的な構成で拡張性に優れている。

- - - -
これはサンプルだけでは勿体無いな。標準の NSresponder の拡張機能としてこのプラグイン構造をサポートしてくれると良かった。

...と書いていたら iOS の UIGestureRecognizer を思い出した。
UIGestureRecognizer Class Reference

UIGestureRecognizer は 3.2 から導入されていて複数タッチを意味のあるジェスチャーイベントとして処理する仕組みがある。これはまさに LightTable の考え方に近い仕組みでもう少し洗練させたものと考えていい。UIGestureRecognizer ではサブクラスが何種類か用意されていて標準的なジェスチャーの処理に利用できる。
UITapGestureRecognizer
UIPinchGestureRecognizer
UIRotationGestureRecognizer
UISwipeGestureRecognizer
UIPanGestureRecognizer
UILongPressGestureRecognizer
使い方は LightTable と同様に UIGestureRecognizer のインスタンスを作成し、コールバックメソッド(セレクタ)を渡しておく。そしてそのインスタンスを UIView へ渡すだけ。LightTable では内部に NSMutableArray を持って自前で InputTracker の管理を行っていたが UIView の場合はお任せで管理コードを書く必要がない。これはいい。Lion(10.7) には 是非 UIGestureRecognizer相当の仕組み(NSGestureRecognizer?)を導入して欲しいな。
このエントリーをはてなブックマークに追加

トラックパッド - 2本指で回転

[関連]

10.6 から追加されたトラックパッドを使った回転操作のイベントを確認してみた。

回転 (rotate)


2本指をトラックパッドへ置いた後、指を左右に回転させる。システム環境設定の「トラックパッド」で有効無効の設定が可能。iPhone ではあまり使われていない(というか見た覚えがない)。

ここで無効になっていると後述する回転時のイベントが受け取れない。




サンプル


初期画面。

トラックパッドで2本指を右に回すと画像が右に回転する。



実装


イベントをハンドリングしたいビューの -[NSResponder rotateWithEvent:] をオーバライドするだけで良い。
- (void)rotateWithEvent:(NSEvent *)event {

    [selfsetBoundsRotation:([selfboundsRotation] + [event rotation]*2)];
    [selfsetNeedsDisplay:YES];

}
以下は渡ってくる NSEvent のログ例。
NSEvent: type=Rotate loc=(95,224) time=148745.0 flags=0x100
 win=0x10061cd10 winNum=16416 ctxt=0x0
 rotation=-0.223812 // 右回転
NSEvent: type=Rotate loc=(265,249) time=148817.8 flags=0x100
 win=0x10061cd10 winNum=16416 ctxt=0x0
 rotation=0.355826 // 左回転


ソースコード


GitHub からどうぞ。
TrackpadSample at 2011-01-30 from xcatsan/MacOSX-Sample-Code - GitHub


参考情報


Cocoa Event-Handling Guide: Handling Trackpad Events
"Handling Gesture Events" に rotationWithEvent: の説明あり。

[Mac] トラックパッド 〜 ピンチで拡大縮小

2011年1月29日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[関連] Cocoaの日々: [Mac] トラックパッドのスワイプのイベントを受け取る

10.6 から追加されたトラックパッドを使ったピンチ操作のイベントを確認してみた。

ピンチ (pinch)


iPhone でお馴染みの2本指を使った拡大縮小操作。2本指をトラックパッドへ置いた後、指を近づけると縮小操作(pinch-in)となる。システム環境設定の「トラックパッド」で有効無効の設定が可能。

ここで無効になっていると後述するピンチのイベントが受け取れない。


サンプル


初期画面。
トラックパッドでピンチイン(2本指を近づける)と縮小する。

ピンチアウト(2本指を遠ざける)と拡大する。




実装


イベントをハンドリングしたいビューの -[NSResponder magnifyWithEvent:] をオーバライドするだけで良い。
- (void)magnifyWithEvent:(NSEvent *)event {
    NSSize newSize;
    newSize.height = self.frame.size.height * ([event magnification] + 1.0);
    newSize.width = self.frame.size.width * ([event magnification] + 1.0);
    [selfsetFrameSize:newSize];
}
event.magnification は縮小の場合負値、拡大の場合正値となる。1.0 を加えて使う。

以下は渡ってくる NSEvent のログ例。
NSEvent: type=Magnify loc=(207,200) time=148622.8 flags=0x100
 win=0x10061cd10 winNum=16416 ctxt=0x0 deltaZ=1.998901
 magnification=0.003998 // 拡大
NSEvent: type=Magnify loc=(191,179) time=148703.6 flags=0x100
 win=0x10061cd10 winNum=16416 ctxt=0x0 deltaZ=-0.999451
 magnification=-0.001999 // 縮小

ソースコード


GitHub からどうぞ。
TrackpadSample at 2011-01-29 from xcatsan/MacOSX-Sample-Code - GitHub


参考情報


Cocoa Event-Handling Guide: Handling Trackpad Events
"Handling Gesture Events" に maginifyWithEvent: の説明あり。

[Mac] NSTableView - ヘッダのカスタマイズ [5] スクロールバーの上の四角〜cornerView

2011年1月28日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回]

cornerView とは?


NSTableView には cornerView というメソッドでビューを取得できる。このビューは何かというとスクロールバー上の四角のエリアにあるヤツを指す。
ヘッダをカスタマイズする場合はここの描画もカスタマイズ内容に合わせておいた方が良い(でないと上記のように色違いになってしまう)。

cornerView はカスタムビューを割り当てることができるのでボタンとして機能させることもできる。Xcodeで cornerView の活用例を見ることができる。
 


実装


今回はカスタムビュー CustomCornerView を作りこれを NSTableView へ割り当てた。
@interface CustomCornerView : NSView {

}

@end
描画コードは前回までの CustomHeaderCell の背景と同じなので、これをクラスメソッドとして切り出た後呼び出すようにした。
@interface CustomHeaderCell : NSTableHeaderCell {
   :
+ (void)drawBackgroundInRect:(NSRect)rect hilighted:(BOOL)hilighted;

@end
@implementation CustomCornerView

- (void)drawRect:(NSRect)dirtyRect {
 
 [CustomHeaderCell drawBackgroundInRect:self.bounds
         hilighted:NO];
}
  :
@end
なおセルが描画される NSTableHeaderView では上下反転(flipped)しているので、CustomCornerView も合わせておく。
- (BOOL)isFlipped
{
 return YES;
}
NSTableView への設定は初期化コード内で行う。
@implementation CustomTableView
   :
- (void)_setupCornerView
{
 NSView* cornerView = [self cornerView];
 CustomCornerView* newCornerView =
  [[CustomCornerView alloc] initWithFrame:cornerView.frame];
 [self setCornerView:newCornerView];
 [newCornerView release];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
 self = [super initWithCoder:aDecoder];
 
 if (self) {
  [self _setupHeaderCell];
  [self _setupCornerView];
 }
 return self;
}
さて動かしてみよう。
出た。


ソースコード


GitHub からどうぞ。
CustomHeaderSample at 2011-01-28 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSTableView - ヘッダのカスタマイズ [4] ソートマーク▼の描画

2011年1月27日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] NSTableView - ヘッダのカスタマイズ [3] ハイライト処理

前回までのサンプルではソートを表す三角▼▲が表示されていない。今回はこれを表示する。


ソート状態の表示


NSTableHeaderCell の -drawWithFrame:inView: をオーバライドすると、ソート指定時に表示される三角が表示されなくなる。NSTableHeaderCell 内で三角形を描画する –drawSortIndicatorWithFrame:inView:ascending:priority: を呼び出す必要がある。

ただこの時困るのは NSTableHeaderCell がソートの状態(ascending, priority)を持っていないこと。NSTableView にも NSTableColumn にもそれらしいメソッドは提供されていない(※ -[NSTableColumn sortDescriptorPrototype] は雛形であって現在の状態ではない)。

ネットで探すと NSTableHeaderCell 自体にソート状態を持たせる方法が紹介されていた。
Re: Correctly Subclassing NSTableHeaderCell?

これを試してみる。


実装


まず NSTableHeaderCell に _ascending(昇順降順区別)、_priority(ソート順)の2つのメンバ変数を加え、設定メソッドを追加する。
@interface CustomHeaderCell : NSTableHeaderCell {

 BOOL _ascending;
 NSInteger _priority;
}

- (id)initWithCell:(NSTableHeaderCell*)cell;
- (void)setSortAscending:(BOOL)ascending priority:(NSInteger)priority;

@end
次に描画メソッド内から -drawSortIndicatorWithFrame:cellFrame:inView:controlView:ascending:priority: を呼ぶ。
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
 [self _drawInRect:cellFrame hilighted:NO];
 [self drawSortIndicatorWithFrame:cellFrame
         inView:controlView
         ascending:_ascending
       priority:_priority];
}

- (void)highlight:(BOOL)flag withFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
 [self _drawInRect:cellFrame hilighted:YES];
 [self drawSortIndicatorWithFrame:cellFrame
         inView:controlView
         ascending:_ascending
       priority:_priority];
}
これでセル側の準備ができた。後はヘッダがクリックされた時にソート状態を更新する必要がある。これは NSTableViewDelegate の -tableView:didClickTableColumn: でクリックイベントを拾って対処する。
#pragma mark -
#pragma mark NSTableViewDelegate
- (void)tableView:(NSTableView *)tableView didClickTableColumn:(NSTableColumn *)tableColumn
{
 CustomHeaderCell* cell = nil;
 BOOL ascending;
 NSInteger priority;
 
 for (NSTableColumn* column in [tableView tableColumns]) {
  
  cell  = (CustomHeaderCell*)[column headerCell];
  
  if (column == tableColumn) {
   ascending = [[[arrayController_ sortDescriptors] objectAtIndex:0] ascending];
   priority = 0;
  } else {
   priority = 1;
  }
  
  [cell setSortAscending:ascending priority:priority];
 }
}
使用している NSArrayController から第1ソート情報を取得し、それをヘッダセルへ反映する。priority はソートで使用するカラムの順番を現していて 0 が第1ソートカラムとなる。通常は priorityが 0 のカラムのみ三角マークを付ける。
さて、これで三角が現れるようになった。クリックすると昇順と降順が切り替わる。
ただデフォルトの三角マークは黒なので背景が黒っぽいと視認しにくい。
そこで -drawSortIndicatorWithFrame:cellFrame:inView:controlView:ascending:priority: をオーバーライドして自前で描画してやる。こんな感じ。
#define TRIANGLE_WIDTH 8
#define TRIANGLE_HEIGHT 7
#define MARGIN_X  4
#define MARGIN_Y  5
- (void)drawSortIndicatorWithFrame:(NSRect)cellFrame
 inView:(NSView *)controlView ascending:(BOOL)ascending priority:(NSInteger)priority
{
 NSBezierPath* path = [NSBezierPath bezierPath];
 
 if (ascending) {
  NSPoint p = NSMakePoint(cellFrame.origin.x +
                        cellFrame.size.width - TRIANGLE_WIDTH - MARGIN_X,
        cellFrame.origin.y + cellFrame.size.height - MARGIN_Y);
  [path moveToPoint:p];
  
  
  p.x += TRIANGLE_WIDTH/2.0;
  p.y -= TRIANGLE_HEIGHT;
  [path lineToPoint:p];
  
  p.x += TRIANGLE_WIDTH/2.0;
  p.y += TRIANGLE_HEIGHT;
  [path lineToPoint:p];
  
 } else {
  NSPoint p = NSMakePoint(cellFrame.origin.x + cellFrame.size.width - TRIANGLE_WIDTH - MARGIN_X,
        cellFrame.origin.y + MARGIN_Y);
  [path moveToPoint:p];
  
  
  p.x += TRIANGLE_WIDTH/2.0;
  p.y += TRIANGLE_HEIGHT;
  [path lineToPoint:p];
  
  p.x += TRIANGLE_WIDTH/2.0;
  p.y -= TRIANGLE_HEIGHT;
  [path lineToPoint:p];
  
 }
 
 [path closePath];
 
 if (_priority == 0) {
  [[NSColor whiteColor] set];
 } else {
  [[NSColor clearColor] set];
 }
 [path fill];
}
するとこうなる。
出た。

ソースコード


GitHub からぞうぞ。
CustomHeaderSample at 2011-01-27 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSTableView - ヘッダのカスタマイズ [3] ハイライト処理

2011年1月26日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] NSTableView - ヘッダのカスタマイズ [2] グラデーション

前回までのコードだとヘッダを選択した時に元のスタイルの表示に戻ってしまう。
選択された時もちゃんと描画するように手を加える。こうなる。

選択時の状態は NSCell における hilighed 状態を指す。この時の描画メソッドは専用に用意されているのでこれをオーバライドする。
- (void)highlight:(BOOL)flag withFrame:(NSRect)cellFrame inView:(NSView *)controlView
;
描画内容は普通の状態の色(黒)を濃くしたものする。これらなら前回のコードに手を入れれば簡単にできる。前回までのメソッド _drawInrect: を拡張して hilighted:(BOOL)hilighted を追加する。
- (void)_drawInRect:(NSRect)rect hilighted:(BOOL)hilighted
{
 CGFloat delta = hilighted ? -0.1 : 0;
 NSArray* colorArray = [NSArray arrayWithObjects:
         [NSColor colorWithDeviceWhite:0.6+delta alpha:1.0],
         [NSColor colorWithDeviceWhite:0.3+delta alpha:1.0],
         [NSColor colorWithDeviceWhite:0.2+delta alpha:1.0],
         nil];
 NSGradient* gradient = [[NSGradient alloc] initWithColors:colorArray];
 [gradient drawInRect:rect angle:90.0];
           :
これを呼び出して終わり。
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
 [self _drawInRect:cellFrame hilighted:NO];
}

- (void)highlight:(BOOL)flag withFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
 [self _drawInRect:cellFrame hilighted:YES];
}

ソースコード


GitHub からどうぞ。
CustomHeaderSample at 2011-01-25 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSTableView - ヘッダのカスタマイズ [2] グラデーション

2011年1月25日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] NSTableView - ヘッダのカスタマイズ [1] NSTableHeaderCell

デザインにグラデーションを使い見た目をもう少し良くする。こんな感じ。
before:
after:


グラデーション


グラデーションを描画するには 10.5 から導入された NSGradient を使うと簡単にできる。
[参考] (旧) Cocoaの日々: NSGradiation

前回の描画コードに下記のコードを追加する。
NSArray* colorArray = [NSArray arrayWithObjects:
         [NSColor colorWithDeviceWhite:0.6 alpha:1.0],
         [NSColor colorWithDeviceWhite:0.3 alpha:1.0],
         [NSColor colorWithDeviceWhite:0.2 alpha:1.0],
         nil];
 NSGradient* gradient = [[NSGradient alloc] initWithColors:colorArray];
 [gradient drawInRect:rect angle:90.0];
これだけ。階調を具合をノンリニアにする為に色の指定を3つ行っている。またセルの境界を綺麗に見せる為に、上と下、左に灰色の線を引いている。さらに線が引き締まって見えるようにアンチエイリアスを切って描画する。
NSGraphicsContext* gc = [NSGraphicsContext currentContext];
 [gc saveGraphicsState];
 [gc setShouldAntialias:NO];
 
 NSBezierPath* path = [NSBezierPath bezierPath];
 [path setLineWidth:1.0];
 NSPoint p = NSMakePoint(rect.origin.x, rect.origin.y+2.0);
 [path moveToPoint:p];
 
 p.y += rect.size.height-2.0;
 [path lineToPoint:p];
 p.x += rect.size.width;
 [path lineToPoint:p];
 
 p = NSMakePoint(rect.origin.x, rect.origin.y+1.0);
 [path moveToPoint:p];
 p.x += rect.size.width;
 [path lineToPoint:p];
 
 [[NSColor colorWithDeviceWhite:0.0 alpha:0.2] set];
 [path stroke];
 
 [gc restoreGraphicsState];


ソースコード


GitHub からどうぞ。
CustomHeaderSample at 2011-01-25 from xcatsan/MacOSX-Sample-Code - GitHub


参考情報


(旧) Cocoaの日々: HUD用のボタンを作る
グラデーションつながりで。ここではグラデーション付きのボタンの描画を紹介している。

NSGradient Class Reference

開発中の SimpleCap 1.3 でも活用中。

[Mac] NSTableView - ヘッダのカスタマイズ [1] NSTableHeaderCell

2011年1月24日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

NSTableView のヘッダをカスタマイズする。

サンプル


通常だとこんな感じ。
これを背景色とフォントを変えてみる。


NSTableViewの構成


NSTableView のヘッダ描画は NSTableHeaderView が管理しているが、実際の描画はこの NSTableHeaderView が NSTableHeaderCell に委託している。そんな構成なので NSTableHeaderCell は NStableHeaderView から得られると思いきやそうではなく、NSTableColumn が参照を持っている。図解するとこんな関係。

この関係からヘッダをカスタマイズする場合は次の手順を踏めば良いことがわかる。

  1. NSTableView から NSTableColumn を取得
  2. NSTableColumn から NSTableHeaderCell を取得
  3. NSTableHeaderCell の draw系メソッドをオーバーライド


NSTableHeaderCell は NSCell のサブクラスなので通常のセルと同じように標準で用意されている描画系メソッドをオーラーライドすれば表示内容をカスタマイズできる。主な描画系メソッドは2つ:
-drawInteriorWithFrame:inView:
-drawWithFrame:inView:
前者はセルの内側のエリアのみ(枠部分を除く)。後者はセルの前エリアの描画。標準の実装では -drawWithFrame:inView: の中で、背景と枠を描画した後 -drawInteriorWithFrame:inView: を呼び出しているようだ。前者でオーバーライドした場合はこんな感じになる。
今回は後者のメソッドをオーバーライドする。


実装


今回は NSTableView と NSTableHeaderCell のサブクラスを用意した。まずは NSTableView のサブクラス。
@interface CustomTableView : NSTableView {

}
@end
- (void)_setupHeaderCell
{
 for (NSTableColumn* column in [self tableColumns]) {
  NSTableHeaderCell* cell = [column headerCell];
  CustomHeaderCell* newCell = [[CustomHeaderCell alloc] init];
  [newCell setAttributedStringValue:[cell attributedStringValue]];
  [column setHeaderCell:newCell];
  [newCell release];
 }
 
}


- (id)initWithCoder:(NSCoder *)aDecoder
{
 self = [super initWithCoder:aDecoder];
 
 if (self) {
  [self _setupHeaderCell];
 }
 return self;
}
Nib から生成される初期化タイミングで NSTableHeaderCell を CustomHeaderCell に置き換えている。この時、既存の NSTableHeaderCell の情報(attributedStringValue)を持ち越している。こうすると Interface Builder で設定したヘッダの文字列や位置決め(中央寄せ)などの情報を反映させることができる。

次は NStableHeaderCell のサブクラス。こんな感じ。
@interface CustomHeaderCell : NSTableHeaderCell {

}
@end
@implementation CustomHeaderCell

- (void)_drawInRect:(NSRect)rect
{
 // 背景描画
 [[NSColor orangeColor] set];
 NSRectFill(rect);
 
 // 文字描画
 NSMutableAttributedString* attributedString =
 [[NSMutableAttributedString alloc] initWithAttributedString:[self attributedStringValue]];

 NSDictionary* attributes =
 [NSDictionary dictionaryWithObjectsAndKeys:
  [NSColor whiteColor], NSForegroundColorAttributeName,
  [NSFont boldSystemFontOfSize:14.0], NSFontAttributeName,
  nil];
 [attributedString addAttributes:attributes
         range:NSMakeRange(0, [attributedString length])];
 
 rect.origin.y += 3;   // Y位置を調整(中央寄せになるように)
 [attributedString drawInRect:rect]; 
}

#pragma mark -
#pragma mark Overridden methods (NSCell)
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
 [self _drawInRect:cellFrame];
}

@end
-drawWithFrame:inView: をオーバーライドし、背景と文字描画を行っている。ここでは NSAttributedString のフォントと文字色の属性を変更している。ちなみにデフォルトのフォントは「Default Helvetica 12-point」。なお NSAttributedString で setAttributes:range: を使うと既存の属性も含めた置き換えになるので注意。


ソースコード


GitHub からどうぞ。
CustomHeaderSample at 2011-01-24 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSScrollView - スクロール時のイベントを受け取る

2011年1月23日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

てっきり NSScrollViewDelegate があって、そこでスクロールイベントが受け取れるものだと思っていたら NSScrollViewDelegate なんてのは存在しないことに気がついた。※iOS の場合だと UIScrollView には UIScrollViewDelegate が存在して、そこのメソッドでイベントを受け取れる。

スクロールイベントを取りたい場合、直接 NSScrollView を使うのではなく、NSScrollView が内包している NSView の通知を利用する。利用方法は公式リファレンスにサンプルのコードが参考になる。
Scroll View Programming Guide for Cocoa: Synchronizing Scroll Views

使う通知は NSViewBoundsDidChangeNotification。これを NSScrollViewの中のビューへ仕掛けておく。こんな感じ。
-(void)awakeFromNib {
 :
NSView* contentView = [scrollView contentView];

[[NSNotificationCenter defaultCenter] addObserver:self
 selector:@selector(didScroll:)
 name:NSViewBoundsDidChangeNotification
 object:contentView];
 :
}
NSScrollView 内のビューの bounds に変化があった時に通知がくる。
- (void)didScroll:(NSNotification*) notification 
{
 NSClipView* contentView = [notification object];
  :
}

- - - -
こうやって APIを比較すると一から作り直した UIKit は、従来のCocoa(AppKit)の良い点をうまく取り込み、悪い点を改善しているのがよくわかる。Cocoaも 10.6から Delegateをベースとした形になっているが過渡期で綺麗じゃないといった感じは否めない。

[Mac] トラックパッドのスワイプのイベントを受け取る

2011年1月22日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

MacBook Pro の何が便利かといえばトラックパッドでスワイプが使えること。環境設定のトラックパッドで動作を設定できる。

Safari などのブラウザで左右にスワイプすると過去のページへ行ったり来たりできるし、Xcodeだと上下のスワイプでヘッダ(*.h)と実装(*.m)ファイルを切り替えることができて非常に便利。ただ2本指のスクロールと違い、スワイプはアプリ側で対応している必要がある。

スワイプは 10.6 から導入された NSResponder の新しいメソッド -swipeWithEvent: を追加するだけで簡単に使うことができる。
NSResponder Class Reference

このメソッドを NSWindow もしくは NSView のサブクラスで実装しておくと、スワイプ発生時に呼び出される。
- (void)swipeWithEvent:(NSEvent *)event
{
    if ([event type] == NSEventTypeSwipe) {  // ※これは不要かもしれない
        // スワイプされた時の処理
    }
}
スワイプの方向は引数で渡される NSEvent の値 deltaX と deltaY で判断できる。上方向へスワイプした場合には deltaY == -1.0 となる。絶対値は常に 1.0のようだ。以下は NSEvent の例。
NSEvent: type=Swipe loc=(325,153) time=35110.6
 flags=0x100 win=0x1436e0 winNum=5070 ctxt=0x214cf
 deltaX=-1.000000 deltaY=0.000000
なおスワイプは 10.6から導入されたイベントなので 10.5互換で開発している場合、NSEventTypeSwipe は定義が無い指摘されてコンパイルエラーとなる。この場合は自分で定義しておく。
#ifndef NSEventTypeSwipe
#define NSEventTypeSwipe 31
#endif

SimpleCap - 1.3開発〜途中経過[2]

2011年1月21日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: SimpleCap - 途中経過

昨年リリースするつもりだったのがまだ完成していません。。

導入を検討していた SHE(Selection History Expose)は見送り。

見た目はいいのだが実用的じゃないというのがその理由。

代わりに Selection History List パネルを追加。


範囲プリセットもここで管理するようにした。


Selection History List は、過去の範囲を選択するとその時に保存した時画像がチラッと表示される。


来月中にはリリースしたい。

[iOS] バックグラウンドへ切り替わる時に最後に表示されていた画面のスクリーンショットが保存されるらしい

2011年1月20日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

元ネタはここ
What’s in Your iOS Image Cache?

気になったので調べてみた。

調査


条件は Fast App Switch(だったけ?)に対応していること。具体的にはInfo.plist に UIApplicationExitsOnSuspend が無いか、チェックが外れていること。
自分で開発したアプリだとオーガナイザを使って iPhone 上の該当アプリのディレクトリを接続している Macへごっそりダウンロードすることができる。

そこでアプリの開発版をXcodeから iPhoneへインストールして実行、適当な画面を表示させた後、ホームボタンを押して一時的に終了させる(これでバックグラウンドへ移行する)。

次にオーガナイザを開き、目的のアプリを見つけてダウンロードする。
ダンロードしたフォルダを開くと
あった。Library/Caches/Snapshots/(identifier)/UIApplicationAutomaticSnapshotDefault-Portrait.jpg という名前で保存されている。開くと確かにアプリ切り替え直前の画像だ。
アプリを完全に終了させるとこの画像は削除されるのが確認できた。

この画像は再びこのアプリに切り替わった時の初期表示(アニメーションなど)で使われるようだ。


考察


通常は問題にならないと思われるが、セキュリティ上もしくはプライバシー上これを厳しく(?)問題視する場合もあるかもしれない。元ネタのサイトでは UIApplicationDelegate の applicationDidEnterBackground: で残したくない情報を非表示にする方法が紹介されていた。またバックグラウンド非対応のアプリ(UIApplicationExitsOnSuspend=NO)だとスクリーンショットは作られない(らしい)。


参考まで。

[Mac][iOS] NSPredicate - SUBQUERY の書き方

2011年1月19日水曜日 | Published in | 4 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(訂正〜サブクエリーの利用)

SUBQUERY情報


前回使った SUBQUERY の情報は NSPredicate の話題を扱っている "Predicate Programming Guide" には無くて、NSExpression のクラスリファレンスに記載されていた。

NSExpression Class Reference

書式はこう
SUBQUERY(collection_expression, variable_expression, predicate);
variable_expression は predicate 内で collection_expression を参照する時の名前を記述する。
前回の設定を例に出すとこんな感じ。
SUBQUERY(books, $s, $s.date >= %@ AND $s.date < %@).@count > 0
$s が books の(一種)別名となり、これを条件内で $s として参照している。SUBQUERYの結果に対して集計関数を適用することができる。例では @count(レコード数)関数を適用して「0件以上」という条件にしている。

[参考情報] Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(集計関数使用)


以下、リファレンスページから例文を転載して紹介する。
(SUBQUERY(residents, $x, $x.firstname == "Jane" && $x.lastname == "Doe").@count != 0)
こうも書けるらしい。
(SUBQUERY(residents, $x, $x.firstname == "Jane" && $x.lastname == "Doe")[size] != 0)


参考情報


Predicate Programming Guide: Introduction to Predicates Programming Guide

.

[Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(訂正〜サブクエリーの利用)

2011年1月18日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本

先日の見本が間違っていることが発覚。先日のやつはこんな指定だった。
p = [NSPredicate predicateWithFormat:
  @"ANY books.date >= %@ AND ANY books.date < %@", date1, date2]
この時の SQLはこうなる。
SELECT DISTINCT 0, t0.Z_PK FROM ZAUTHOR t0
 JOIN ZBOOK t1 ON t0.Z_PK = t1.ZAUTHOR
 JOIN ZBOOK t2 ON t0.Z_PK = t2.ZAUTHOR
 WHERE ( t1.ZDATE >= ? AND  t2.ZDATE < ?)
よくよく考えると同じテーブルではあるが、別個に結合(JOIN)しているので、WHERE句の AND 条件が意図通りに働かない。実際、テストしていて意図しないレコードがヒットするのに気がついた。これは WHERE句内の2つの条件が(元は同じだが)別のテーブル(集合)として扱われる為。 ただ先日示したように ANY(条件 AND 条件) は使えない(パースエラーになる)。この場合 SUBQUERY を使う。SUBQUERYを使って書くと先ほどの設定はこうなる。
p = [NSPredicate predicateWithFormat:
 @"SUBQUERY(books, $s, $s.date >= %@ AND $s.date < %@).@count > 0", date1, date2];
SQLはこうなる。
SELECT 0, t0.Z_PK FROM ZCUSTOMER t0
 WHERE (SELECT COUNT(*) FROM ZKARTE t1
  WHERE (t0.Z_PK = t1.ZCUSTOMER
  AND (( t1.ZTREATEDDATE >= ? AND  t1.ZTREATEDDATE < ?))) ) > ? 
これなら問題ない。実際の動作でも意図通りとなった。


参考情報


iphone - Core Data ANY BETWEEN predicate - Stack Overflow
まったく同じ問題で困っていた人がいた。

git-flow 使用開始

2011年1月17日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: git-flow をインストール

既存(といっても始めたばかりの)gitリポジトリに途中から git-flow を適用してみた。
$ cd project
$ git flow init
fatal: Working tree contains unstaged changes. Aborting.
変更途中のファイルが存在したので怒られた。すべてコミットして再び init を実行。
$ git flow init

Which branch should be used for bringing forth production releases?
   - master
Branch name for production releases: [master] 
Branch name for "next release" development: [develop] 

How to name your supporting branch prefixes?
Feature branches? [feature/] 
Release branches? [release/] 
Hotfix branches? [hotfix/] 
Support branches? [support/] 
Version tag prefix? [] 
$
ブランチを見ると develop と master が用意されていて、現在は developがチェックアウトされている。
$ git branch
* develop
  master
あとはこの develop で開発を進めていけばいい。git-flow については今後開発の節目節目でまた紹介したいと思う。


参考情報


A successful Git branching model を補助する git-flow を使ってみた - Twisted Mind
分かりやすくて参考になる。

- - - -
既存のリポジトリでも導入は難しくなさそうなので、良さそうなら他のプロジェクトにも導入して行きたい(というか、運用中のプロジェクトの方が git-flowの恩恵が受けられると思われる)。

git-flow をインストール

2011年1月16日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: MacPorts と git インストール

git-flow をインストールした。

A successful Git branching model


"A successful Git branching model" とは Vincent Driessenが紹介している git を効果的に利用したソースコード管理モデルのこと。下記のサイトで公開されている。
上記は英語だが、@oshow さんが翻訳したものを公開している。
見えないチカラ: A successful Git branching model を翻訳しました

特徴的なのはブランチの使い方で、master, develop の2つのブランチを中心に、新機能開発用の feature、バグ修正用の hotfixes など用途に合わせたブランチの種類を定義している。これらのブランチの定義ならびに運用方法を定義したのがこのモデル。


git-flow


git-flow は "A successful Git branching model" を実践するのに便利なツール集。GitHubでソースが公開されている。
nvie/gitflow - GitHub

活用例がいろいろなサイトで紹介されているが下記が分かりやすくて参考になる。
A successful Git branching model を補助する git-flow を使ってみた - Twisted Mind

インストール方法は何種類かあるが今回は MacPorts を使った。
$ sudo port install git-flow
Password:
--->  Computing dependencies for git-flow
--->  Dependencies to be installed: getopt
--->  Fetching getopt
--->  Attempting to fetch getopt-1.1.4.tar.gz from http://distfiles.macports.org/getopt
--->  Verifying checksum(s) for getopt
--->  Extracting getopt
--->  Applying patches to getopt
--->  Configuring getopt
--->  Building getopt
--->  Staging getopt into destroot
--->  Installing getopt @1.1.4_1
--->  Activating getopt @1.1.4_1
--->  Cleaning getopt
--->  Fetching git-flow
--->  Verifying checksum(s) for git-flow
--->  Extracting git-flow
--->  Configuring git-flow
--->  Building git-flow
--->  Staging git-flow into destroot
--->  Installing git-flow @0.4_0
--->  Activating git-flow @0.4_0
--->  Cleaning git-flow
$
前回のハマりがウソのように今回は一発ですんなり入った。/opt/local/bin 配下に git-flow* ファイルがインストールされる。
$ cd /opt/local/bin
[bin]$ ls git-flow*
git-flow  
git-flow-init  
git-flow-version
git-flow-feature 
git-flow-release
git-flow-hotfix  
git-flow-support

- - - -
新規にとりかかっている iPadアプリ開発でこれを試してみる。

MacPorts と git インストール

2011年1月15日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

nvie/gitflow - GitHub を試そうと思ったところ MacPorts版があるようなので、良い機会なので MacPorts を MacBook Proへインストールすることにした。

MacPorts


公式ページから Snow Leopard 用のパッケージ(1.9.2) をダウンロードしてインストールした。
The MacPorts Project -- Home


git のインストール


以前、ソースコードからインストールしたが((旧) Cocoaの日々: github を使う)これを削除して MacPorts経由でインストールする。

[参考にしたページ] clmemo@aka: MacPorts で Snow Leopard に git をインストールした

$ sudo port install git-core
Password:<パスワード入力>
Warning: xcodebuild exists but failed to execute
Warning: Xcode does not appear to be installed; most ports will likely fail to build.
--->  Computing dependencies for git-core
--->  Dependencies to be installed: curl curl-ca-bundle perl5 perl5.8 libidn gettext expat libiconv gperf ncurses ncursesw openssl zlib pkgconfig p5-error python27 bzip2 db46 gdbm readline sqlite3 rsync popt
--->  Fetching perl5.8
   :
(以下、依存パッケージのインストールが続く)

環境によっては ncources 辺りでハマるらしいが素の MacBook Pro では特に問題は起きなかった。
MacPorts でgit をインストールするのが大変なわけ | ブログが続かないわけ

代わりに Xcode に関してエラー。インストール場所が標準と異なる為のようだ。
--->  Extracting zlib
Error: Couldn't find Xcode; expected it to be at /Developer/Developer Tools/
  Xcode and iOS SDK 3.2.5/Applications/Xcode.app.
Error: 
Error: If you have not installed Xcode, install it now; see:
Error: http://guide.macports.org/chunked/installing.xcode.html
Error: 
Error: If you have installed Xcode in a nonstandard location, inform MacPorts
Error: of that location by changing the 'developer_dir' variable in
Error: /opt/local/etc/macports/macports.conf
自分の環境では SDKを /Developer Tools/Xcode and iOS SDK 3.2.5 に入れてある。MacPorts は xcode-select というコマンドの結果を使っているらしい。これを見ると存在しないフォルダになっていた(近いのだが)。
$ xcode-select -print-path
/Developer/Developer Tools/Xcode and iOS SDK 3.2.5
これを次のように切り替える。
$ sudo xcode-select -switch /Developer\ Tools/Xcode\ and\ iOS\ SDK\ 3.2.5
[/]$ xcode-select -print-path
/Developer Tools/Xcode and iOS SDK 3.2.5
後々 Xcodeのバージョンが上がればまた同じ問題は出るのだが、とりあえず今回はこのまま進む。git-core のインストールを再開する。
sudo port install git-core
--->  Computing dependencies for git-core
--->  Dependencies to be installed: curl openssl zlib pkgconfig p5-error python27
 bzip2 db46 gdbm readline sqlite3 rsync popt
--->  Extracting zlib
  :
順調に進んだかと思ったが今度は db46 というとろろでエラー。
--->  Attempting to fetch bzip2-1.0.6.tar.gz from http://distfiles.macports.org/bzip2
--->  Fetching db46
--->  Attempting to fetch patch.4.6.21.1 from http://download.oracle.com/berkeley-db/patches/db/4.6.21/
--->  Attempting to fetch patch.4.6.21.2 from http://download.oracle.com/berkeley-db/patches/db/4.6.21/
--->  Attempting to fetch patch.4.6.21.3 from http://download.oracle.com/berkeley-db/patches/db/4.6.21/
--->  Attempting to fetch patch.4.6.21.4 from http://download.oracle.com/berkeley-db/patches/db/4.6.21/
--->  Attempting to fetch db-4.6.21.tar.gz from http://download-east.oracle.com/berkeley-db/
--->  Verifying checksum(s) for db46
--->  Extracting db46
--->  Applying patches to db46
--->  Configuring db46
Error: db46 requires the Java for Mac OS X development headers.
Error: Download the Java Developer Package from: 
Error: Target org.macports.configure returned: missing Java headers
Error: Failed to install db46
 :
Java用ヘッダファイルのインストールが必要とある(なんじゃそりゃ!)。ネットで情報を探すと同じ現象にあった人の記事が見つかった。

試験管のなかのコード :: MacPorts の "db46 requires the Java..." エラー

Java for Mac OS X 10.6 を入れないと駄目らしい。ADC Member サイト内の Downloads & ADC Program Assets - Java から下記の Java for Mac OS X 10.6 (10M330) をダウンロードしてインストールする。

インストール後、git-core のインストールを再開。
$ sudo port install git-corePassword:
--->  Computing dependencies for git-core
--->  Dependencies to be installed: python27 db46 gdbm readline sqlite3 rsync popt
--->  Configuring db46
--->  Building db46
  :
 :
--->  Fetching git-core
--->  Attempting to fetch git-1.7.3.5.tar.bz2 from http://distfiles.macports.org/git-core
--->  Attempting to fetch git-manpages-1.7.3.5.tar.bz2 from http://distfiles.macports.org/git-core
--->  Attempting to fetch git-htmldocs-1.7.3.5.tar.bz2 from http://distfiles.macports.org/git-core
--->  Verifying checksum(s) for git-core
--->  Extracting git-core
--->  Applying patches to git-core
--->  Configuring git-core
--->  Building git-core
--->  Staging git-core into destroot
--->  Installing git-core @1.7.3.5_0+doc+python27
--->  Activating git-core @1.7.3.5_0+doc+python27
--->  Cleaning git-core
$
終わった。途中の対応を除いても実に 40分近くはかかった。

インストールされた git は /opt/local/bin に配置されている。
$ which git
/opt/local/bin/git
git のバージョンは 1.7.3.5。前回ソースでいれた時よりも結構バージョンが上がっている。
$ git --version
git version 1.7.3.5

- - - -
普段から MacPorts を使っていれば今回みたいに依存パッケージを一気にダウンロードするということは無いと思うので、すんなり行くのかもしれない。何はともあれ良かった。

[iOS] UIWebView のキャッシュ調査

2011年1月14日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

UIWebView のキャッシュについて少し調べた。

概要


[関連情報] Cocoaの日々: サーバ上のファイルを見るには?

UIWebView は指定されたリクエストに従いコンテンツをダウンロードする。この時にキャッシュをメモリに保存する。

キャッシュの制御には UIWebViewに渡す NSURLRequest で行う。
(例)
 NSURLRequest *request = [NSURLRequest requestWithURL:url
   cachePolicy:NSURLRequestReturnCacheDataElseLoad
   timeoutInterval:60.0];
キャッシュポリシーは次の種類がある。NSURLRequst.h より転載。
enum
{
    NSURLRequestUseProtocolCachePolicy = 0,

    NSURLRequestReloadIgnoringLocalCacheData = 1,
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    NSURLRequestReturnCacheDataElseLoad = 2,
    NSURLRequestReturnCacheDataDontLoad = 3,

    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};
typedef NSUInteger NSURLRequestCachePolicy;
デフォルトでは NSURLRequestUseProtocolCachePolicy になっている。


キャッシュの仕組み


キャッシュは URL Loading System の一部として働く。
URL Loading System Programming Guide: URL Loading System Overview

キャッシュ動作については以前 Mac OS X で調査したことがある。
(旧) Cocoaの日々: キャッシュ調査(さらに続く)

iOSでも同等の仕組みを使っているので基本的には同じ動作となる。以下、要点を抜粋:
・キャッシュはディスクとメモリを使う。
・キャッシュはアプリケーションごとに格納される。
・キャッシュは NSURLConnection もしくは NSURLDownload によって使われる
・キャッシュポリシーは NSURLRequest 初期化時に決められる
・NSURLCache クラスを使ってキャッシュサイズや、ディスク上の保存位置を設定できる
・NSCachedURLResponse を使うとキャッシュされたコンテンツへアクセスすることができる
・NSCachedURLResponse は NSURLResponse とコンテンツデータをカプセル化している
・現行は http/https のリクエストがキャッシュされる
 (httpsリクエストはディスクにキャッシュされることはない)
・NSURLConnection の connection:willCacheResponse: デリゲートメソッドを使って
 キャッシュの挙動を制御することができる
iOS の場合は下記の点が異なる。
・ディスクキャッシュは使わない
・-[NSURLCache cachedResponseForRequest:] をオーバーライドして
 キャッシュの挙動を制御することができる。
ディスクキャッシュを使わないことはリファレンスでは確認できなかったが、いくつかのブログで指摘されていた。

A-Liaison BLOG: iPhoneやiPhoneシミュレータ上でNSURLCacheクラスを使う
iPhone実機では、メモリ上へのキャッシュは働くがファイル上へのキャッシュは行われない。したがってアプリを終了するとキャッシュはすべて消える。
objective c - iphone: NSURLCache on disk - Stack Overflow
AFAIK NSURLRequest/NSURLConnection on iOS doesn't support caching to disk.
iPhoneシミュレータではディスクキャッシュが残るらしい。


応用(1) カスタムキャッシュ


UIWebView は暗黙的に NSURLCache を使用している。
NSURLCache Class Reference

通常はシステムがインスタンスを提供していて +sharedURLCache で利用することができる。このインスタンスは +setSharedURLCache: で置換することができるので、キャッシュ動作をカスタマイズしたい場合は自分で NSURLCache のサブクラスを用意し、そのインスタンスを設定することで URL Loading System(すわなわちそれを利用する UIWebView)のキャッシュの振る舞いをカスタマイズすることができる。
@interface CustomCache : NSURLCache
{
  :
}
@end
設定はアプリケーション起動直後などで行う。
- (BOOL)application:(UIApplication *)application 
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    :
    CustomCache *cache =
      [[CustomCache alloc] initWithMemoryCapacity: memoryCapacity
                         diskCapacity: discCapacity diskPath:path];
    [NSURLCache setSharedURLCache:cache];
    [cache release];
}

UIWebView はコンテンツを取得する場合、次の動作を行う。
1. loadRequest: が呼ばれる
2. UIWebView内で -[NSURLCache cachedResponseForRequest:] を呼び出す。
 a. 戻り値の NSCachedURLResponse(キャッシュされたコンテンツ)が取得できればそれを利用する。
 b. 戻り値が nil(キャッシュ無し)ならネットワーク経由で最新のコンテンツを取得する。
この為、NSURLCahce のサブクラスでは -[NSURLCache cachedResponseForRequest:] をオーバーライドして、ここにキャッシュロジックを記述する。iOS ではディスクキャッシュを使わないが、必要なら自前でディスクキャッシュをここに実装することもできる。またUIWebView を使ったオフライン動作するようなアプリもこれを使うと組めそうだ。

カスタムキャッシュクラスの作成方法は下記のページが役に立つ。
IPHONE - How to save the content in UIWebView for faster loading on next launch? - efreedom

応用例まとめ


  • カスタムキャッシュロジックの実装(ディスクキャッシュなど)
  • UIWebView のオフライン動作(キャシュコンテンツの強制利用、HTML中のリソースリンクの書き換えなど)



応用(2) コンテンツフィルタ


NSURLCache のサブクラス化を応用するとコンテンツフィルタを実装することができる。コンテンツフィルタを行う方法は下記が参考になる。
URL filtering for UIWebView on the iPhone – iCab Blog

(cachedResponseForRequest: の実装を転載)
- (NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest*)request
{
    NSURL *url = [request URL];
    BOOL blockURL = [[FilterMgr sharedFilterMgr] shouldBlockURL:url];
    if (blockURL) {
        NSURLResponse *response =
              [[NSURLResponse alloc] initWithURL:url
                                        MIMEType:@"text/plain"
                           expectedContentLength:1
                                textEncodingName:nil];

        NSCachedURLResponse *cachedResponse =
              [[NSCachedURLResponse alloc] initWithResponse:response
                             data:[NSData dataWithBytes:" " length:1]];

        [super storeCachedResponse:cachedResponse forRequest:request];

        [cachedResponse release];
        [response release];
    }
    return [super cachedResponseForRequest:request];
}
FilterMgr によてブロックすべきURLと判断した場合は空白のページを返す様になっている。つまりURLの実体とは無関係に、プログラムで勝手に表示したいコンテンツをキャッシュと偽装して UIWebViewへ渡すことでフィルタリングを実現している。
このアイディアは面白い。この方法を使えばコンテンツ中のタグの書き換えも容易にできる(実際にその用途でよく利用されているらしい)。


応用例まとめ

  • コンテンツの差し替え
  • コンテンツの書き換え(リンクの書き換え、メッセージの書き換え)



参考情報


iPhone, Android, webOS モバイルブラウザキャッシュの制限 - WebOS Goodies
モバイル Safari 他のキャッシュについての調査。

(旧) Cocoaの日々: WebKit検証(32) - キャッシュの中身
Cache.db の調査。

(旧) Cocoaの日々: WebKit検証(36) - キャッシュをクリアする(成功)

NSURLCacheについてのメモ - iRSSの日記
キャッシュはLast-ModifiedやExpiresヘッダがないと機能しないのである。

Cocoa with Love: Substituting local data for remote UIWebView requests

[Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(集計関数使用)

2011年1月13日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本

お題


今回は最も新しい本の発売日が指定した日以前の著者一覧を取得する、という条件を作ってみる。例えば 2010年10月2日を指定した場合、最も新しい本が 10月2日以前の著者が該当する。11月1日に本を発売している著者はヒットしない。


条件見本


"最も新しい" 日付を条件に使うために max() 関数を使う。
NSPredicate* p = [NSPredicate predicateWithFormat:@"max(books.date) <= %@", date];
集計関数 max() を使うので ANY は必要ない。 発行される SQLはこんな感じ。
SELECT 0, t0.Z_PK FROM ZAUTHOR t0 WHERE
 (SELECT MAX(t1.ZDATE)  FROM ZBOOK t1
   WHERE (t0.Z_PK = t1.ZAUTHOR) ) <= ?
サブクエリーで子テーブルBOOKから最も新しい日付を抽出し、それと条件を比較している。

なお NSPredicate には NONE というキーワードも用意されている。
Predicate Programming Guide: Predicate Format String Syntax

これを使うと下記のようにも書けそうである。
NSPredicate* p = [NSPredicate predicateWithFormat:@"NONE books.date > %@"];
指定日時よりも新しい発売日が無いと意味(=最も新しい発売日が指定日時以前)。
ただ実際には意図通りには動かない。SQLを見ると単純に NOT が付くだけのようだ。
SELECT DISTINCT 0, t0.Z_PK FROM ZAUTHOR t0
  JOIN ZBOOK t1 ON t0.Z_PK = t1.ZAUTHOR
  WHERE  NOT (t1.DATE > ?)


NSPredicate 内で利用可能な関数


NSPredicate で使える関数は max()の他、count, min, sum などがある。 以下、Predicate Programming Guide: BNF Definition of Cocoa Predicates の Function Name から転載。
function_name ::= "sum" | "count" | "min" | "max"
    | "average" | "median" | "mode" | "stddev"
    | "sqrt" | "log" | "ln" | "exp"
    | "floor" | "ceiling" | "abs" | "trunc"
    | "random" | "randomn" | "now"
count, min などの集計関数の他、sqrt, log など数値変換用の関数も用意されている。

サーバ上のファイルを見るには?

2011年1月12日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

頭の整理。

UIWebView に任せるのが一番簡単。
もう一つはダウンロードを自前でやる方法。
ビューをUIWebView以外に差し替えられる。自由度を考えるとこっちかな。

とっても簡単なアーキテクチャだけど図に書きだすと頭の中がすっきりした。

[Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本

2011年1月11日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

2011-01-20 追記
ここに書かれている方法には誤りがあります。正しい方法を別途書きましたので、そちらも合わせて読んで下さい。
Cocoaの日々: [Mac][iOS] NSPredicate - 1対多関連のエンティティの検索条件見本(訂正〜サブクエリーの利用)


お題


こんな1対多のエンティティがあったとする。
発売日(Book.date)が指定日以降の本を書いた著者(Author)の一覧を取得したい。この時の条件(NSPredicate)はどう書くべきか。


条件見本


NSPredicate ではキーパスを指定できるので日付の条件はこう書ける。
books.date <= %@
Author に対してこの条件を投げるわけだが、今回結果として欲しいのは著者の一覧なので該当する Bookが存在する Authorだけが帰ってくるようにしたい。この場合は ANY をつけてやる。
ANY books.date <= %@
Author を対象とした検索条件 NSPredicate はこうなる。
NSPredicate* p = [NSPredicate predicateWithFormat:@"ANY books.date <= %@", date];
実際に発行されているSQL文を確認してみよう。
SELECT DISTINCT 0, t0.Z_PK FROM ZAUTHOR t0
  JOIN ZBOOK t1 ON t0.Z_PK = t1.ZAUTHOR
  WHERE t1.ZDATE >= ?
親子のテーブルをJOINして、条件にマッチしたレコードをDISTINCTを使って重複をなくしている。意図通りだ。SQLのEXISTSを使えばもう少し効率を上げられそうではあるが件数が多くなければこれでも十分。


条件が複数


もう一つ条件を加えて、ある期間内に発売された本を書いた著者の一覧を取得する。単純に条件を加えてみるとこんな感じ。
ANY (books.date >= %@ AND books.date < %@)
これは実はNG。NSInvalidArgumentException (Unsupported predicate) が発生する。正解はこう。
ANY books.date >= %@ AND ANY books.date < %@
ANY をそれぞれの頭につけてやる。

SQLはこうなる。
SELECT DISTINCT 0, t0.Z_PK FROM ZAUTHOR t0
 JOIN ZBOOK t1 ON t0.Z_PK = t1.ZAUTHOR
 JOIN ZBOOK t2 ON t0.Z_PK = t2.ZAUTHOR
 WHERE ( t1.ZDATE >= ? AND  t2.ZDATE < ?)
2回 JOINをしているのがアレだが、まあ意図通りの動きにはなるならない。→ 冒頭に記載された最新情報を参照してください。


備考


範囲条件指定には BETWEEN というのもある。
(例)
NSNumber *one = [NSNumber numberWithInteger:1];
NSNumber *ten = [NSNumber numberWithInteger:10];
NSPredicate *betweenPredicate = [NSPredicate predicateWithFormat:
     @"attributeName BETWEEN %@",
     [NSArray arrayWithObjects:one, ten, nil]];
ただ NSDate を条件にした場合例外が出てしまった。使い方がまずいのか仕様なのか。

.

[Mac][iOS] NSCompoundPredicate で条件をまとめる

2011年1月10日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

Core Data を使ったアプリケーションで下のような検索機能を実装している。

設定された値を元に NSPredicate を作成し、Core Data に対して検索をかけるのだが、こういう場合に NSCompoundPredicate が役に立つ。
NSCompoundPredicate Class Reference

各々の条件は必ずしも指定されるわけではないので検索条件の項目は可変となる。普通の NSPredicate を使う場合、条件の有無によって文字列の連結を使って条件を構成する必要がある。こんな感じ。
NSMutableString* fmt = [NSMutableString string];
NSMutableArray* array = [NSMutableArray array];
if (name) {
    [fmt appendString:@"name == %@"];
    [array addObject:model.name];
}
if (customerType) {
    if ([fmt lenght] > 0) {
        [fmt appendString:@" AND "];
    }
    [fmt appendString:@"customerType == %@"];
    [array addObject:model.customerType];
  :

NSPredicate* predicate = [NSPredicate predicateWithFormat:fmt argumentArray:array];
これはこれで動くのだが NSCompoundPredicate を使うともっと簡潔に書ける。こんな感じ。

NSMutableArray* array = [NSMutableArray array];
if (name) {
    [array addObject:[NSPredicate predicateWithFormat:@"name == %@", model.name]];
}
if (customerType) {
    [array addObject:[NSPredicate predicateWithFormat:@"customerType == %@",
        model.customerType]];
}
  :
NSPredicate* predicate = [NSCompoundPredicate andPredicateWithSubpredicates:array];
上記は AND接続だったが他に OR, NOT が用意されている。

(a AND b) OR (c AND b) とやりたい場合は NSCompundPredicate を複数使えば良い。
NSPredicate* a, b, c, d;
a = ....;
b = ....;
c = ....;
d = ....;
NSPredicate* r1, r2, r3;
r1 = [NSPridicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:a, b, nil]];
r2 = [NSPridicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:c, d, nil]];
r3 = [NSPridicate orPredicateWithSubpredicates:[NSArray arrayWithObjects:r1, r2, nil]];
条件内のカッコ( ) は自動的に付く。組み立てた条件は NSLogで NSPredicate を表示すれば確認できる。

notPredicateWithSubpredicate: を使った場合、頭に NOT が付く。
NSPredicate* a = [NSPredicate predicateWithFormat:@"name == %@", name];
NSPredicate* r = [NSCompoundPredicate noPredicateWithSubpredicate:a];
この時 r は NOT (name == @"hogehoge") となる。

[iOS] JSON Framework を使う

2011年1月9日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

JSON を扱う必要が出てきたので試してみた。


JSON Framework


下記から入手できる。
JSON Framework

今回は 2.3.1 を試してみた。


インストール


インストール方法は付属の Installation.markdown に記述されている。一番簡単なのはソースコードをコピーする方法。

後は使いたい箇所でインポートして必要なメソッドを呼び出すだけ。
#import "JSON.h"


サンプル


入力したテキストを JSON としてパースし、その結果をデバッグコンソールへ書きだすサンプルを作ってみた。
- (IBAction)parse:(id)sender
{
 id result = [self.textView.text JSONValue];
 NSLog(@"%@", result);
}

JSONを書きこんでパースすると

こんな結果。日本語(UTF-8)も大丈夫そうだ。
JSONFrameworkSample[14416:207] (
        {
        id = 10331;
        name = "\U5168\U54e1\U66f8\U304d\U8fbc\U307f";
        "ssl_cert_flag" = 0;
    },
        {
        id = 10348;
        name = "\U65b0\U30d7\U30ed\U30b8\U30a7\U30af\U30c8";
        "ssl_cert_flag" = 0;
    }
)
なおキーをダブルクォーテーションで囲わない場合エラーとなった。
JSONFrameworkSample[14416:207] -JSONValue failed. Error trace is: (
    "Error Domain=org.brautaset.JSON.ErrorDomain Code=3 \"Object key string expected\"
 UserInfo=0x4bc50e0 {NSLocalizedDescription=Object key string expected}",
    "Error Domain=org.brautaset.JSON.ErrorDomain Code=3 \"Expected value while parsing array\"
 UserInfo=0x4bc5390 {NSUnderlyingError=0x4bc5350 \"Object key string expected\",
 NSLocalizedDescription=Expected value while parsing array}"
)
JSONの仕様上、文字列はダブルクォーテーションで囲うことになっている為。


ソースコード


GitHub からどうぞ。
JSONFrameworkSample at 2011-01-09 from xcatsan/iOS-Sample-Code - GitHub


参考情報


A strict JSON parser and generator for Objective-C
クラスリファレンス

[iPhone] JSON Framework の使い方(解析編) | Sun Limited Mt.
使い方の簡単な解説など

Tutorial: JSON Over HTTP On The iPhone

[iOS] UIWebView で画像(*.jpg)を表示する

2011年1月8日土曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [iOS] UIWebView で動画(*.mov)を表示する

画像が表示できるのはわかっているが挙動を念のため確認。

サンプル


- (IBAction)load7:(id)sender
{
 NSURL* url = [[NSBundle mainBundle] URLForResource:@"sample"
           withExtension:@"jpg"];
 NSURLRequest* req = [NSURLRequest requestWithURL:url];
 
 [self.webView loadRequest:req]; 
 
}

iPhoneシミュレータでの動作結果。
等倍で表示された。ピンチによる拡大縮小はできなかった。画像のビューアとしてはちょっと役不足か。他の PDF, Excel等も確認したが同様に拡大縮小ができなかった。基本的に UIWebView ではピンチによる拡大・縮小には対応していないということか。


ソースコード


GitHubからどうぞ
DisplayingExcelFile at 2011-01-08 from xcatsan/iOS-Sample-Code - GitHub

[iOS] UIWebView で動画(*.mov)を表示する

2011年1月7日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [iOS] UIWebView でPDFを表示する

今回は動画を表示してみた。

サンプル


iPhone 3GS で撮影した動画ファイル(sample.mov)を UIWebViewで読み込む。
- (IBAction)load6:(id)sender
{
 NSURL* url = [[NSBundle mainBundle] URLForResource:@"sample"
           withExtension:@"mov"];
 NSURLRequest* req = [NSURLRequest requestWithURL:url];
 
 [self.webView loadRequest:req]; 
 
}

iPhoneシミュレータでの動作結果。
終了後の状態。
デバッグコンソールにこんなメッセージが表示されていた。
DisplayingExcelFile[9610:207] setting movie path:
 file:///Users/hashi/Library/Application%20Support/iPhone%20Simulator/4.2/
 Applications/26D1D860-3C33-4AD0-B052-9F182D7E642D/DisplayingExcelFile.app/sample.mov

iPad シミュレータでの結果


ソースコード


GitHubからどうぞ
DisplayingExcelFile at 2011-01-07 from xcatsan/iOS-Sample-Code - GitHub

人気の投稿(過去 30日間)