ページ

ラベル BlogAssistant の投稿を表示しています。 すべての投稿を表示
ラベル BlogAssistant の投稿を表示しています。 すべての投稿を表示

2010年2月5日金曜日

BlogAssistant(14) - Queueの導入: フォルダ監視(FSEvent)

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

(前回)Cocoaの日々: BlogAssistant(13) - Queueの導入: plistの読み込みと CoreDataへの保存

次は FSEvent を使ってフォルダを監視し、自動的にplistを読み込むようにする。

FSEvent の使い方は以前紹介したことがある。
Cocoaの日々: FSEvent - フォルダを監視する

処理は QueueManager に記載する。

QueueManager.m

- (void)startObservingQueue
{
 if (fseventStream) {
  FSEventStreamStop(fseventStream);
  FSEventStreamInvalidate(fseventStream);
 }
 
 NSString* path = [[PathManager sharedManager] queuePath];
 NSArray* pathsToWatch = [NSArray arrayWithObjects:path, nil];
 void *selfPointer = (void*)self;
 FSEventStreamContext context = {0, selfPointer, NULL, NULL, NULL};
    NSTimeInterval latency = 1.0; /* Latency in seconds */
 
    /* Create the stream, passing in a callback */
 fseventStream = FSEventStreamCreate(NULL,
    &fsevents_callback,
    &context,
    (CFArrayRef)pathsToWatch,
    kFSEventStreamEventIdSinceNow,
    latency,
    kFSEventStreamCreateFlagNone /* Flags explained in reference */
    );

    /* Create the stream before calling this. */
 FSEventStreamScheduleWithRunLoop(fseventStream,
   CFRunLoopGetCurrent(),
   kCFRunLoopDefaultMode
   );
 
 FSEventStreamStart(fseventStream);
 
}

監視間隔は1秒とした。このメソッドをアプリケーション起動時に呼び出しておく。

コールバックされる関数はこんな感じ。
void fsevents_callback(
   ConstFSEventStreamRef streamRef,
   void *userData,
   size_t numEvents,
   void *eventPaths,
   const FSEventStreamEventFlags eventFlags[],
   const FSEventStreamEventId eventIds[])
{
 QueueManager* qm = (QueueManager*)userData;
 [qm loadFiles];
}

前回作成した -[QueueManager loadFiles] を呼び出すだけ。


さて実行してみよう。
適当なページでクリップすると
出た。いい感じだ。

なおコールバック関数は1回のクリップ毎に2回呼ばれる。これはプラグインが Queueフォルダ内に plistを作成した1回に加え、管理アプリが読み込み後にplistを削除する1回があるため。削除時にイベントを無視できればよいのだが FSEvent を見る限りではそこまでフィルタできないようだ。呼び出している loadFiles はフォルダ内にファイルがなければすぐに終わる処理なのでそんなに問題にはならないだろう。そのままとしておく。


ソースコード (GitHub)

xcatsan's BlogAssistant at 2010-02-05 - GitHub

2010年2月4日木曜日

BlogAssistant(13) - Queueの導入: plistの読み込みと CoreDataへの保存

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

(前回)Cocoaの日々: Cocoaの日々: BlogAssistant(12) - Queueの導入: plistの保存

Safariプラグインで保存した plist を読み出し、CoreDataへ保存する実装を管理アプリで行う。

まず QueueManager を作った。ここで plist の読み込みから CoreDataへの保存までのコントロールを行う。
QueueManager.m

- (void)loadFiles
{
 NSFileManager* fm = [NSFileManager defaultManager];
 NSString* path = [[PathManager sharedManager] queuePath];
 NSError* error = nil;

 NSArray* files = [fm contentsOfDirectoryAtPath:path
     error:&error];
 if (error) {
  NSLog(@"ERROR(loadFiles): %@", error);
  return;
 }
 ModelManager* mm = [ModelManager sharedManager];
 for (NSString* filename in files) {
  if ([[filename pathExtension] isEqualToString:@"plist"]) {
   ResourceTransfer* resTran =
    [ResourceTransfer resourceTransferWithContentsOfFile:filename];
   if (resTran) {
    [mm insertResourceWithTransfer:resTran];
   }
  }
 }
}

まず所定のフォルダ(Queue)内の plist ファイルの一覧を作る。次に plist 毎に ResourceTransfer を作成する。そしてそれを ModelManager に渡して CoreData へ保存する。

plist から ResourceTransfer を作るメソッドはこんな感じ。

ResourceTransfer.m
+ (ResourceTransfer*)resourceTransferWithContentsOfFile:(NSString*)filename
{
 return [[[ResourceTransfer alloc] initWithContentsOfFile:filename] autorelease];
}
- (id)initWithContentsOfFile:(NSString*)filename
{
 self = [super init];
 if (self) {
  NSString* path = [[[PathManager sharedManager] queuePath] stringByAppendingPathComponent:filename];
  NSDictionary* inputDict = [NSDictionary dictionaryWithContentsOfFile:path];
  if (inputDict) {
   for (NSString* key in [inputDict allKeys]) {
    [self setValue:[inputDict valueForKey:key] forKey:key];
   }
  }
 }
 return self;
}


CoreDataへの保存は -[ModelManager insertResourceWithTransfer:] で行う。

ModelManager.m
-(BOOL)insertResourceWithTransfer:(ResourceTransfer*)resTran
{
 NSManagedObjectContext* moc = [[CoreDataManager sharedManager] managedObjectContext];

 Resource* resource =
 (Resource*)[NSEntityDescription insertNewObjectForEntityForName:@"Resource"
            inManagedObjectContext:moc];

 NSArray* keys = [Utility getPropertyNamesOf:resTran];
 for (NSString* key in keys) {
  [resource setValue:[resTran valueForKey:key] forKey:key];
 }
 NSError* error = nil;
 
 [moc save:&error];
 
 if (error) {
  NSLog(@"ERROR(insertResourceWithTransfer:): %@", error);
  return NO;
 }
 
 return YES;
}

+[Utility getPropertyNamesOf:] は以前紹介した class_copyPropertyList( ) を使ってプロパティ一覧を取得するユーティリティ。前回も ResourceTransfer 内で使っていたがリファクタリングして Utilityクラスへ移した。

Utility.m
+ (NSArray*)getPropertyNamesOf:(id)object
{
 NSMutableArray* propertyNames = [NSMutableArray array];

 unsigned int outCount, i;
 objc_property_t *properties =
 class_copyPropertyList([object class], &outCount);
 
 for(i = 0; i < outCount; i++) {
  objc_property_t property = properties[i];
  NSString *propertyName =
  [NSString stringWithUTF8String:property_getName(property)];
  [propertyNames addObject:propertyName];
 }
 free(properties);
 
 return propertyNames;
}

クラスの依存関係はこんな感じ。

ResourceTransfer と Resource の間の依存関係は無い。この間の依存関係を知っているのは ModelManager だけ。


実行してみよう。Safariでクリップしておき、管理アプリ側で -[QueueManager loadFiles] を呼び出す。

表示された。動いているようだ。


ソースコード(GitHub)
xcatsan's BlogAssistant at 2010-02-04 - GitHub

2010年2月2日火曜日

Cocoaの日々: BlogAssistant(12) - Queueの導入: plistの保存

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

(前回)Cocoaの日々: BlogAssistant(11) - 複数のプロセスからCoreData を使う 〜 Queue(もどき)の導入

プラグイン側でクリップした情報を plist形式で書き出す。アプリとやり取りするためのフォルダを Images と同じレベルで用意した。

場所:~/Application Support/BlogAssistant/Queue

フォルダ名は PathManager に管理させ、他のクラスはこれを参照する(-[PathManager queuePath])。存在しない場合は自動的に作られる。


プラグイン側では新規にモデルクラス ResourceTransfer を用意した。

ResourceTransfer.h
@interface ResourceTransfer : NSObject {

 NSDate * createdDate;
 NSString * imageFilename;
 NSString * title;
 NSString * url;
 NSString * uuid;
}
@property (retain) NSDate * createdDate;
@property (retain) NSString * imageFilename;
@property (retain) NSString * title;
@property (retain) NSString * url;
@property (retain) NSString * uuid;

-(BOOL)save;

@end

このクラスで Queue へ保存する plistファイルを作成する。このクラスは Resourceクラスとほぼ同じプロパティを持つ。本来なら Resource クラスで一元化できれば良いのだが、NSManagedObject のサブクラスである Resource を使うには Core Data Stack 一式を用意する必要がある。単にデータホルダーとしての役割の為だけに Core Data を使うのは無駄なのでメンテナンスの手間はかかるが別クラスとした。ビューアアプリ側では後ほどこのクラスを使って plistの内容から ResourceTransfer を作成し、さらにそこから Resource インスタンスを生成し CoreDataへ保存する。

また結果的にプラグイン側では Core Data をまったく使わなくなった。これに伴ない関係クラスやフレームワークをビルド対象から外した。このことでプラグインがコンパクトになり不安定になる要因を減らせたのは良かった。


save メソッドはこんな感じ。

ResourceTransfer.m
-(BOOL)save
{
 NSMutableDictionary* outputDict = [NSMutableDictionary dictionary];
 unsigned int outCount, i;
 objc_property_t *properties =
  class_copyPropertyList([self class], &outCount);
 
 for(i = 0; i < outCount; i++) {
  objc_property_t property = properties[i];
  NSString *propertyName =
   [NSString stringWithUTF8String:property_getName(property)];
  [outputDict setObject:[self valueForKey:propertyName]
        forKey:propertyName];
 }
 free(properties);

 NSString* filename = [self.uuid stringByAppendingPathExtension:@"plist"];
 NSString* path = [[[PathManager sharedManager] queuePath] stringByAppendingPathComponent:filename];
 return [outputDict writeToFile:path atomically:YES];
}
プロパティを NSMutableDictionary へ詰めて、それを -[NSMutableDictionary writeToFile:atomically:] で plist形式で書き出す。プロパティの一覧取得にはランタイム関数の class_copyPropertyList()を使った。 (参考)Cocoaの日々: @property の一覧を取得する
なおサムネイル画像の書き出しや読み込みは専用のクラスを用意してそちらにまかせるようにした。

ImageManager.h
@interface ImageManager : NSObject {

}
+ (ImageManager*)sharedManager;
-(NSImage*)thumnailImageFromView:(NSView*)view;
- (BOOL)writeView:(NSView*)view withFilename:(NSString*)filename;
- (NSImage*)readImageWithFilename:(NSString*)filename;

@end

最後にプラグインのメイン処理。だいぶすっきりした。
PluginController.m
-(void)addPage:(id)sender
{
 // determine view to save
 WebView* web_view = [sender representedObject];
 WebFrameView* frame_view = [[web_view mainFrame] frameView];
 NSView* doc_view = [frame_view documentView];

 // setup output data
 ResourceTransfer* resTran = [[[ResourceTransfer alloc] init] autorelease];
 resTran.title = [web_view mainFrameTitle];
 resTran.url = [web_view mainFrameURL];

 // save image file
 BOOL r1, r2;
 r1 = [[ImageManager sharedManager] writeView:doc_view
     withFilename:resTran.imageFilename];

 // save data
 if (r1) {
  r2 = [resTran save];
          :


さて実行してみよう。ページを開いてクリップする。

サムネイル画像が保存され
plistができた。
中身を見る。
いいようだ。

後はこれをビューアアプリで読み取ればいい。


コードは GitHub からどうぞ。
xcatsan's BlogAssistant at 2010-02-02 - GitHub

- - - -
なお Queueに格納するデータ形式は Cocoaの KeyedArchiver も考えたが、plist(XML)の方がデバッグしやすいし可搬性が高いので採用した。ChromeやFireFoxのプラグインなどでも同じ形式が書き出せればSafari以外でも使えるようになる、なんて目論見もある。

2010年1月29日金曜日

BlogAssistant(11) - 複数のプロセスからCoreData を使う 〜 Queue(もどき)の導入

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

Cocoaの日々: BlogAssistant(10) CoreData - 複数のプロセスから CoreDataを扱う 〜 NSManagedObjectのリロード

前回は1つの CoreData DB(SQLite)を複数のプロセスからアクセスする際に、NSManagedObjectContextを作り直して表示更新を行うことを試みた。



その後いろいろ考えたが、やはり正規の方法から外れるのは今後何かと問題になりそうなので、CoreData DB を操作するのは基本一つのプロセスに限定することにした。ただ IPCやその他の実装を使った同期式のクライアント=サーバモデルは、大げさすぎるし運用もしづらい。そこで簡単な非同期式の連携方法である Queueモデルを採用することにした。


こうすることで CoreData DB(SQLite)へのアクセスを一元化できる。非同期なのでリアルタイムではなくなるがファイルがよほど溜まらない限りはそんなに遅延は発生しないだろう。また Queueといっても特別な仕組みを作るのではなく、特定のフォルダを用意してそこでファイルをやりとりするだけ(このフォルダが簡易 Queueの役割を果たす)。FSEventを使えば効率的に監視ができる。

- - -
まずは監視の部分は置いておいて、ファイルのやりとり部分から実装をやって行く事にする。ファイルは取り扱いが容易な plistを使うことにする。

(続く)

2010年1月28日木曜日

BlogAssistant(10) CoreData - 複数のプロセスから CoreDataを扱う 〜 NSManagedObjectのリロード

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

Cocoaの日々: BlogAssistant(9) CoreData - awakeFromInsert の使用

BlogAssistant ではSafariプラグインを使ってページをクリップした後、別の専用アプリケーションの方でそれを表示させる。データの受け渡しには CoreData を使う。こんなイメージ。




しかし、CoreData は複数のプロセスから使う様にはできていない。Apple公式のドキュメントには複数プロセスが可能であるとの記載はないし、APIや説明を読む限り複数プロセスから扱うように出来ているとは思えない(ただしマルチスレッドは OK。注意点は色々あるものの、ドキュメントにも説明が割かれている)。

Safariプラグインと、ビューアを分けてしまったものの困ったことになった。このまま無理矢理自己責任で進むか、それとも当初のように Safariプロセス内で全部やるか。うーむ。


とりあえずできるところまで別プロセスでやってみることする。今回は用途はクリティカルではないので最悪DBが壊れても困らない。


さて、その上で最初に解決しなければならないのは、Safariプラグインでクリップした時に書き出したCoreDataの変更を、別プロセスの表示に反映する方法を見つけること。

トリガーは、プラグイン側で書き出し後にビューアアプリをキックするのでいいとして、問題はその後。複数プロセスからの利用を想定していない CoreDataスタックは SQLiteが書き変わったことが検出できないのはもちろんのこと、一発で NSManagedObjectContext にデータを再読み込みさせる方法がない(見つけられなかった)。

NSManagedObjectContext には -[reset] が用意されているが、既にNSManagedObjectContext内にフェッチされていた内容がすべて消えてしまう。再フェッチは自分でやらなければいけないが NSArrayController と連携している場合にやり方がよくわからない。

※探してみると Stack Overflow に同じ目的の質問が出ていた。それに対する答えは、複数プロセスで直接データへアクセスするの無理だろう、片側のプロセスで一括して管理するようにした方がいい(クライアント-サーバモデルを取る)とのこと。
NSManagedObjectContext and NSArrayController reset/refresh problem - Stack Overflow

改めて複数プロセスからのアクセスは無謀であることがわかったが。。


いろいろ考えて思いついたのは NSManagedObjectContext を再作成すること。つまり今まで使っていたものを解放して、同じ NSPersistentCoordinator から新たに NSManagedObjectContext を作成する。そしてそれを NSArrayController へ再設定してやる。

実際のコードはこんな感じ。

CoreDataManager.m

- (NSManagedObjectContext*)recreateContext
{
if (managedObjectContext) {
[managedObjectContext release];
managedObjectContext = nil;
}
return [self managedObjectContext];
}


表示を最新に更新する場合はこれを呼び出して、戻り値で取得した新しい NSManagedObjectContext をビューが参照している NSArrayController へ設定してやれば良い。

コード例:

-(IBAction)reset:(id)sender
{
[arrayController setManagedObjectContext:
[[CoreDataManager sharedManager] recreateContext]];
}



実際に試したところうまく動いた(更新された)。


- - - -
やっておいて何だが、少々荒技でかなりの無駄遣いな方法だと思う。特に件数が多い場合は NSManagedObjectContextを作り直すたびに全件読み込みになるので時間とメモリを使う。メモリは気になるので後で Instruments で調べてみたい。

2010年1月27日水曜日

BlogAssistant(9) CoreData - awakeFromInsert の使用

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

(前回)Cocoaの日々: BlogAssistant(8) - Safariプラグインとビューアの分離

モデルへ新たに作成日時を保持する createdDate を追加した。


現在日時を入れることがわかっているので、NSManagedObjectを新規に作成した時には自動的にセットした。

そういった目的のために NSManagedObject には -[awakeFromInsert] が用意されている。
Mac Dev Center: NSManagedObject Class Reference - awakeFromInsert

NSManagedObject が新規に作成される時に呼び出される。サブクラスでこれをオーバーライドすると初期化処理を書く事ができる。

こんな感じ。

- (void) awakeFromInsert {
    [super awakeFromInsert];
    [self setPrimitiveValue:[NSDate date] forKey:@"createdDate"];
}

上記では createdDate に現在日時をセットしている。


この awakeFromInsert には利用上いくつか注意点がある。
  • 必ず先頭で親クラスの awakeFromInsert を呼び出す
  • [1]作成 => [2]Undo(作成取り消し) => [3]Redo(再作成) の操作を行った場合、[3]では呼び出されない
  • メソッド内で fetch操作を行うと副作用が出る(場合がある)
  • primitive accessor (setPrimitiveValue:forKey:など) を使うのが望ましい。これは Undo/Redoが正しく働かせるため。
最後の点について、通常 NSManagedObject のサブクラスでは属性を @property宣言して、 @dynamic 扱いにするので(つまり CoreDataフレームワークが動的にメソッドを追加する)、恐らく下記コードでも良いと思う。試したところ設定は問題なし。Undo/Redoは試していない。

 self.createdDate = [NSDate date];

2010年1月22日金曜日

BlogAssistant(8) - Safariプラグインとビューアの分離

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

(前回)Cocoaの日々: BlogAssistant(7) - Safari でリンク登録&サムネイル作成

これまではページクリップとその管理をSafari上ですべてやろうと考えていたが、これを分離する。具体的には、Safari上はクリッピングだけに専念し、その管理は別アプリケーション(別プロセス)で行うようにしたい。

理由は Safariの安定性を保つことと、クリップしたデータを管理するアプリケーションを扱いやすくするため。今やブラウザはネットバンキングや商品購入など行う際に重要な役割を担うまでになっているので、これが不安定になると非常にまずい。自分自身、クレジットカード番号を入れている時に落ちたらとても困る。また余計な機能を盛り込むことで何らかのセキュリティホールが生じてもまずい。管理アプリケーション(ビューア)が Safariと同じプロセス上で動作していると特に安定性で問題を生じやすいし、また見た目もわかりずらい(Safariに寄生する形なので)。そんなこんなで結局管理アプリを別に立てることにした。Safariプラグインとの連携は、とりあえずクリップ後に管理アプリケーションを立ち上げるだけにする。

今回はとりあえずプラグインとアプリの完全分離と連携までやってみる。元々1つの同じXcodeプロジェクトで開発していて、テスト用アプリを作るターゲットを用意してあったのでこれを活用する。

まずターゲット名を整理した。




また以前作ったテストアプリ用ターゲットは BlogAssitant Test App.webplugin などと変な名前になっていたのでこれを修正する。ターゲットの情報を開き、ビルドタブを開く。


この中の「プロダクト名」と「ラッパーの拡張子」でアプリ名を変えられる。

同じ場所で実行パッケージに入れる infoファイルの設定もできる。今は BlogAssistant.App-Info.plist が指定されていて、ビルド時にはこれが Info.plist となって実行パッケージにおさめられる。

それぞれのターゲットビルドすると Debugフォルダにこんなファイルができあがる。

プラグイン側の変更点は、以前のビューアを開くコード(1行)を削除し、その代わりに BlogAssistant.appを開くようにする。
[[NSWorkspace sharedWorkspace] openFile:@"" withApplication:@"BlogAssistant.app"];

ビューア(管理アプリ)側は特に変更なし。


さて実行してみる。

Safari でクリップ。

すると管理アプリケーションが立ち上がる。



なお連続してクリックした場合、管理アプリの方の表示が更新されない。この辺りの連携を今後丁寧に書く必要がある。ところで複数のプロセスから CoreDataへアクセスするとどうなるんだろうか。マルチプロセスに対応していなかった気がする。

ソースコード
GitHubからどうぞ
xcatsan's BlogAssistant at 2010-01-22 - GitHub

-----
今後は次の名称を使う。

BlogAssistant -- 管理アプリケーション
BlogAssistant Plug-in -- Safariプラグイン

2010年1月20日水曜日

BlogAssistant(7) - Safari でリンク登録&サムネイル作成

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

(前回)Cocoaの日々: BlogAssistant(6) - リファクタリング 〜 クラス構成の見直し

バグ取りやCoreDataの不審な挙動、画像のflipped問題などなどでずいぶんと手間取ってしまったが、ようやく動く様になってきた。

こんな感じ。


今はデータの追加ができるだけで、リンクのコピーとかはできない。今後ぼちぼち手を入れていく。

以下、開発ログ。


縮小画像を奇麗にする

NSImage を lock して別の NSImage へ描画する方法で縮小を行う場合、デフォルトではあまり奇麗な画像にならない。


[thumnailImage lockFocus];
[viewImage drawInRect:thumnailRect
   fromRect:clippedRect
   operation:NSCompositeSourceOver fraction:1.0];
[thumnailImage unlockFocus];



こんな感じ。

この方法で縮小する場合は -[NSGraphicsContext setImageInterpolation:] を使うと良い。

[thumnailImage lockFocus];
[NSGraphicsContext saveGraphicsState];
[[NSGraphicsContext currentContext] setImageInterpolation:NSImageInterpolationHigh];

[viewImage drawInRect:thumnailRect
    fromRect:clippedRect
    operation:NSCompositeSourceOver fraction:1.0];

[NSGraphicsContext restoreGraphicsState];
[thumnailImage unlockFocus];


するとこうなる。

アンチエイリアスが効いていい感じ。

他のケースも置いておく。

これが


こうなる。

 -[NSGraphicsContext setImageInterpolation:] は以前紹介したことがある。興味がある方はそちらもどうぞ。
Cocoaの日々: キャプチャ画像を縮小して保存する

なお設定するタイミングは -[NSImage lockFocus] の後にする。そうでないと目的のコンテキスト内で有効にならない。


CoreDataの不審な挙動

xcdatamodel ファイル編集画面で、エンティティのクラスを指定することができる。

右上のやつ。

当初クラスは NSManagedObject なのだが、今回サブクラスである Resourceを作ったので、ここを Resource に変えた。すると挙動がおかしくなった。具体的には NSTableView にモデル内のデータが表示されなくなった。ただ見た目は空なのだがデータ件数分レコードが存在し編集もできる。編集すると突然そのレコードだけ表示されるようになる。原因がわからず困っていたが、一旦ファイル(Resource.h/m)を削除して、Xocdeの新規ファイル作成から「管理オブジェクトクラス」として作り直すと問題が出なくなった。

原因わからず。うーむ。



NSImage のフリップ対応

以前 Mac OS X v10.6 から -[NSImage setFlipped] が Deprecated になったことを紹介した。

Cocoaの日々: NSImage の isFlipped/setFlipped: が Deprecated

その対策として座標系を上下反転させて描画すれば良い、とのことだったが座標系が変わると座標計算をし直す必要があり、これに手間取った。

上下反転には NSAffineTransform を使う。こんな感じ。

[NSGraphicsContext saveGraphicsState];


NSAffineTransform* xform = [NSAffineTransform transform];
[xform translateXBy:0.0 yBy:viewSize.height];
[xform scaleXBy:1.0 yBy:-1.0];
[xform concat];
[image drawInRect:imageFrame
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0];
[NSGraphicsContext restoreGraphicsState];



+[NSGraphicContext saveGraphicState] しておかないと、同じビューで描画する他のところでおかしなことになる(座標系が変わるため)。


座標の補正イメージ:

フリップしているVIEWの特定の位置に NSImage を描く場合。そのままだと逆さに表示される。今までは -[NSImage setFlipped:YES] で終わりだったのだが、Deprecated 扱いとなったので自前で反転させる。

NSAffinTransform によって描画時のみ原点が左下に戻る(Cocoaはデフォルトで左下が原点、ビューがフリップさせると左上になる)。この場合、画像の描画位置は先ほどの (ix, iy) ではなく (ix', iy')を指定する必要がある。


計算はこんな感じ。
ix' = ix
iy' = vh - iy - ih


※先の投稿の時は描画位置を気にしていないので特に問題にならなかった。



バグ
表示対象の画像(NSImage)に autoreleaeを付けたため、表示する時にアプリがクラッシュしていた。初歩的ミス。


- (NSImage*)image
{
if (!image && self.imageFilename) {
NSString* path = [[PathManager sharedManager] imagePath];
image = [[[NSImage alloc] initWithContentsOfFile:
  [path stringByAppendingPathComponent:self.imageFilename]] autorelease]; <--ここ
}
return image;
}


なお画像は NSManagedObject のサブクラス Resourceで持っている。これは CoreData とは関係なしにインスタンス変数として存在する。


@interface Resource :  NSManagedObject
{
NSImage* image;
}

@property (retain) NSString * imageFilename;
@property (retain) NSString * title;
@property (retain) NSString * url;

@property (retain) NSImage * image;

@end


インスタンスが解放される時に image を解放する(release)必要がるが、普通に dealloc に書けばいいのだろうか(今はそうしている)。NSManagedObjectは特殊なのでちょっと気になる。※後日しらべる。

Mac Dev Center: NSManagedObject Class Reference - Subclassing Notes



ソース
GitHubからどうぞ。
xcatsan's BlogAssistant at 2010-01-20 - GitHub


- - - -
画像の扱いをどうするか。ブログへアップロードする時にファイル名が UUID じゃさっぱり区別が付かないし。自前でアップロードできると一番いいのだが。

2010年1月13日水曜日

BlogAssistant(6) - リファクタリング 〜 クラス構成の見直し

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

(前回)Cocoaの日々: BlogAssistant(5) - プラグインのローカライズ文字列事情

プラグイン作成中の過程でクラス構成を見直すことにした。具体的には MVCの Modelが責務過剰だったのでこれを見直した。

今まではこんな感じだった。



あきらかに1つのクラスに責務が集中しすぎている。

これを責務に毎に分解した。


  • 名前も Contorller をやめて Manager としてみた(MVCのControllerとごっちゃになるので)。
  • CoreDataManager はCore Data 関連クラスのインスタンスを保持する。他のクラスからの要請に応じてそれを渡すだけの役割を担う。このクラスは汎用的なので他のアプリでも使える。
  • ModelManager はトランザクション管理(今回はSQLiteへの保存と画像保存がセット)やデータに関するヘルパー的な役割を担う。ここがいわゆるビジネスロジックを実装する部分。このアプリケーション専用のロジックが埋め込まれる。またController や View に対するデータ操作のインターフェイスを提供する。
  • ModelManager は今回一つだが、たくさんのモデルを扱うアプリケーションではビジネスロジック毎複数用意するようになる。
  • PathManager はDBファイルや画像ファイルを格納するパスの管理、新規作成する画像ファイル名の発行などを行う。
いずれの Manager もシングルトンで +[sharedManager]でインスタンスを取得してアクセスする。
(例)CoreDataManager* manager = [CoreDataManager sharedManager];

インスタンスを作らずすべてクラスメソッドとグローバル変数で実装する手もあるが、インスタンス化しておいた方が後々拡張しやすいのでこうしておく(インスタンスを複数作成して切り替えたり、実行時にプリファレンスなどによる変更などしやすい)。

それぞれのインターフェイスは現時点でこんな感じ。

CoreDataManager.h

@interface CoreDataManager : NSObject {

    NSPersistentStoreCoordinator *persistentStoreCoordinator;
    NSManagedObjectModel *managedObjectModel;
    NSManagedObjectContext *managedObjectContext;
}
+ (CoreDataManager*)sharedManager;
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator;
- (NSManagedObjectModel *)managedObjectModel;
- (NSManagedObjectContext *)managedObjectContext;

@end


ModelManager.h

@class Resource;
@interface ModelManager : NSObject {

}
+ (ModelManager*)sharedManager;

-(void)save;
-(Resource*)createResource;

@end


PathManager.h

@interface PathManager : NSObject {

NSString* dataPath;
}
@property (retain) NSString* dataPath;

+ (PathManager*)sharedManager;

- (NSString*)imagePath;
- (NSString*)newImageFilename;

@end


全体の構成イメージはこんな感じとなる。


データの保存や読み込みやビジネスロジックなどはすべて右側の modelクラス群が受け持ち(中心的なのは ModelManager となる)、左側の view と controller はそれらを利用するだけ。おおざっぱに言うと、viewはユーザインターフェイスに専念し、コントローラは view と model の紐付け(データの受け渡し)に専念する。


MVCの考え方は下記がとても参考になった。

Life is beautiful: Ruby on Railsの「えせMVC」の弊害

ついつい controller にビジネスロジックが詰め込まれる、という話はよくわかる。
(今回は modelが肥大化したんだけれど)



- - - -
今日は時間切れ。動くところまでコードの実装が終わらなかった。ソースコード公開は明日以降。

2010年1月9日土曜日

BlogAssistant(5) - プラグインのローカライズ文字列事情

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

(前回)Cocoaの日々: BlogAssistant(4) - プラグインのプロトタイプ作成

前回 Safariのコンテキストメニューへメニューを追加した。


次はここを足がかりにして処理を加えていく。その最初の一歩としてメニュー文字列を testではなくきちんとした文言に変えよう。また後々を考えてローカライズしておこう。

まずは(いつものごとく) Localizable.strings を作る。


中身はこんな感じ。最初のうちは英語だけ作っていく。


これを NSLocalizedString( ) で参照する。


item = [[[NSMenuItem alloc] initWithTitle:
  NSLocalizedString(@"MenuAddPage", @"")
   action:@selector(addPage:)
keyEquivalent:@""] autorelease];


実行して確認してみる。


ちょっと違う。

うっかりしていたが NSLocalizedString( ) はメインバンドル内の Localizable.strings を参照するようになっている。プラグインは Safari のコンテキストで動作するので、メインバンドルはこのプラグインのそれではなく Safari のバンドルが使われる。プラグインのバンドル(NSBundle)を明示的に指定してやる必要がある。

同系列の関数には NSLocalizedStringFromTableInBundle( ) があって、こちらだとバンドルを指定することができる。

バンドル(NSBundle)を取得するにはいくつか方法がある。

Mac Dev Center: NSBundle Class Reference



今回は +[bundleForClass:] を使うことにする。また NSLocalizedString( ) のリファレンスによれば、内部的には -[NSBundle localizedStringForKey:value:table:] を呼び出しているとのことだったので、これを使うことにする。

Mac Dev Center: NSBundle Class Reference - localizedStringForKey:value:table:

マクロではなくユーティリティクラスを用意して、そこへメソッドとして実装する。

Utility.m

@implementation Utility

+ (NSString*)localizedStringForKey:(NSString*)key
{
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
return [bundle localizedStringForKey:key value:@"" table:nil];
}

@end


これを使う様に修正を入れ、

item = [[[NSMenuItem alloc] initWithTitle:
  [Utility localizedStringForKey:@"MenuAddPage"]
   action:@selector(addPage:)
keyEquivalent:@""] autorelease];


実行してみよう。

出た。
これで行こう。

2010年1月8日金曜日

BlogAssistant(4) - プラグインのプロトタイプ作成

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

(前回)Cocoaの日々: BlogAssistant(3) - GUIプロトタイプ

前回まで作ったコードを Safari プラグインに組み込む。今回はコンテキストメニューにテスト用のメニューを追加し、それを選択するとウィンドウを表示するようにしてみた。

クラス構成

構成はこんな感じ。※先頭の [Models]などは分類名。

[Models] CoreDataの管理およびモデルクラスなど
  |
  |-- Resource          モデル
  |-- ModelController   CoreData管理

[Viewer] ビューアウィンドウのクラス群
  |
  |-- ViewerController  ビューコントローラ(NSWindowControllerのサブクラス)
  |-- Viewer.xib        Xib/Nibファイル
  |-- CustomCell        カスタムセルクラス
  :      :

[Plugin Classes] プラグインクラス群
  |
  |--SXSafariContextMenuSwizzler  Safariコンテキストメニュー置き換えクラス
  |--PluginController             プラグインコントローラ(メインの制御を担当)

PluginController がメインの制御を受け持ち、ここがプラグイン動作すべてのコントロールを行う。動作イメージはこんな感じ。

[Safari]
  ↓
PluginController ー→ ModelController ー→ [CoreData Stack] 
  ↓               ↑   |
ViewerController ー+    |
  |                    ↓
  |                 NSManagedObjectContext
  ↓                    ↑
Viewer.xib       ー+    |
CustomTableView  ー+→ Resource(モデル)
CustomCell*      ー+

PluginControllerの初期化コード。Safari が最初に +[initialize] を呼び出すところから始まる。

PluginController.m

static BOOL _initialized_flag = NO;
static PluginController* _shared_instance;
+ (void)initialize
{
if (!_initialized_flag) {
_initialized_flag = YES;

// (1) PluginController
// #???: The instance will live until Safari shut down
_shared_instance = [[PluginController alloc] init];

// (2) SXSafariContextMenuSwizzler
[SXSafariContextMenuSwizzler setup];
NSMenuItem* item;
item = [[[NSMenuItem alloc] initWithTitle:@"test"
action:@selector(test:)
keyEquivalent:@""] autorelease];

[item setTarget:_shared_instance];
[SXSafariContextMenuSwizzler addMenuItem:item];

NSLog(@"BLogAssistant was loaded");
}
}

- (id)init
{
self = [super init];
if (self) {
viewerController = [[ViewerController alloc] init];
}

return self;
}



初期化時に必要なインスタンスを作成する。なお ViewerController は NSWindowController のサブクラスで、インスタンス化時に Viewer.xib を読み込む(正確には -[window]メソッドを呼んだとき)。


Safari コンテキストメニューへの追加

Safari のコンテキストメニューへの追加は Method Swizzling という手法を使う。この方法については過去の記事で解説があるので必要な方は参照されたい(右の検索窓で "swizzling" で検索する)。

今回は過去作成した SXSafariContextMenuSwizzler というクラスを流用する。

(参考)
Cocoaの日々: Safari用独自プラグインを作る(10) - コンテキストメニューの調査
Cocoaの日々: Safari用独自プラグインを作る(11) - スクリーンショットを撮る

Swizzling の内容は以前の記事に譲るとして、今回変更を入れた部分はメニューアイテムを外部から登録できるようにしたところ。

SXSafariContextMenuSwizzler.m

static NSMutableArray* _menuItems;


- (NSArray *)_sx_webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element
defaultMenuItems:(NSArray *)defaultMenuItems {
  :

for (NSMenuItem* item in _menuItems) {
[item setRepresentedObject:sender];
[new_menu_items addObject:item];
}
return new_menu_items;
}





+ (void)addMenuItem:(NSMenuItem*)menuItem
{
[_menuItems addObject:menuItem];
}


メニュー配列(_menuItems)が Global変数(疑似クラス変数)になっているのは、実行時にはメソッド入れ替えのために self が SXSafariContextMenuSwizzler のインスタンスではなく、Safari のクラス BrowserWebView のインスタンスになるため。SXSafariContextMenuSwizzler でインスタンス変数を定義しても実行時には参照できない。ややこしいのだが Method Swizzling とはそういうもの(それを忘れていて実は最初インスタンス変数を使って少々ハマってしまった)。実際メソッド内で self を表示させると次のようになる。

Safari[2521] self: <BrowserWebView: 0x4c31c0>


メニューで test が選択されたらログへ -[representedObject] の戻りを表示し、HUDウィンドウを表示する。-[representedObject] からは BroserWebView のインスタンスが取得できる(これはメニューを作る時に仕込んでおく〜SXSafariContextMenuSwizzler.m を参照)。これを後で使って表示中の URL やタイトル、表示view などの情報を得る。

PluginController.m

-(void)test:(id)sender
{
NSMenuItem* item = (NSMenuItem*)sender;
NSLog(@"test: %@", [item representedObject]);
[viewerController window];
}


実行

ビルドしたプラグインを Safari へ組み込んで実行してみよう。Safariを立ち上げる。

右クリックでコンテキストメニューを出す。"test" が最後に現れる。

なおプラグインのクラスが読み込まれるまでにはタイムラグがあって、右クリック初回は出てこない。1回目の後、数秒経ってから右クリックするとようやく出てくる。

"test" を選択するとHUDタイプ(半透明黒)のウィンドウが表示される。




デバッグ

プラグインのデバッグには NSLog( ) を使う。NSLog( ) の出力は コンソール.app というアプリケーションで確認することができる。コンソール.app はアプリケーションフォルダのユーティリティ配下にある。

Safari以外のすべてのログがここに表示されるので、右上のフィルタで Safari 関係のログだけを表示するようにする。

出力はこんな感じ。







(参考)
Cocoaの日々: Plug-in開発で検証・テスト用環境を作る〜 Xcodeで複数のターゲットを設定する

- - - -
HUDウィンドウは、NSWindowController + Xib の組み合わせで作っておいたので、組み込みが数行で済んでしまい非常に楽。データソース(今回はCoreData)との接続も ModelControllerを用意しておけばそれを使ってコントローラ内部で自分で接続してくれる。ModelController はCoreData Stackを扱うシングルトン。ここへ問い合わせれば NSManagedObject が手に入るようにしてある。こんなクラスを用意しておくとNSManagedObjectContext への参照でインスタンス変数を持つべきか、とかで悩まなくて済む。

この辺りのコンポーネント化は慣れると楽で気分がいい。そういえば iPhoneプログラミング もそんな感じだっけ(あっちは UIViewController + Xib の組み合わせが基本)。


ソースコード

GitHub からどうぞ
xcatsan's BlogAssistant at 2009-01-08 - GitHub
(あ、年号古いままだ。。)

2010年1月5日火曜日

BlogAssistant(3) - GUIプロトタイプ

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

(前回まで)
Cocoaの日々: NSTableView にカスタムセルを表示する (14) ボタンをつける〜ボタンの表示とイベント処理
Cocoaの日々: Plug-in開発で検証・テスト用環境を作る〜 Xcodeで複数のターゲットを設定する
Cocoaの日々: HUD用のボタンを作る


前回までの結果を元に BlogAssistant の開発を進めている。リンクを一覧する GUIのプロトタイプができた。こんな感じ。


これはテスト用ビルドでのみ使うDBメンテナンスウィンドウ。




専用のリポジトリを用意した。

GitHub からどうぞ。
xcatsan's BlogAssistant at master - GitHub

- - - -
とりあえず表示側が形になったので、そろそろプラグイン作成に入る。

2009年11月17日火曜日

BlogAssistant(1) - CoreData を NSTableView へ表示させる(NSArrayController経由でバインド)

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

以前、書いたようにブログ書き支援用の小アプリを作る。
Cocoaの日々: SimpleCapへ Webページのスクリーンショット機能を追加する(予定)

HTMLのAタグとサムネイル画像を作るようなアプリを考えている。名前は BlogAssistant にしよう。このデータを CoreData で管理させようと思っていろいろ試行錯誤している。今回は簡単なモデルを作成し、それを NSTableView へ表示させてみよう。

Xcodeで Core Data Application を作成する

(以下、MacOSX10.5、XCode 3.1.1で操作)

新規プロジェクトを作る。後々 Safariのプラグインに変える予定だが CoreDataの挙動を確認しながら作りたいのでまずは単体のアプリケーションとして作る。Xcodeで新規プロジェクトを作成しテンプレートの中から Core Data Application を選択する。



自動的に *_AppDelegate クラスが生成される。


このクラスにはあらかじめ Core Data 永続化スタックを使うのに必要なコードが自動的に用意されている。ヘッダファイルはこんな感じ。
BlogAssistant_AppDelegate.h

#import

@interface BlogAssistant_AppDelegate : NSObject
{
    IBOutlet NSWindow *window;
  
    NSPersistentStoreCoordinator *persistentStoreCoordinator;
    NSManagedObjectModel *managedObjectModel;
    NSManagedObjectContext *managedObjectContext;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator;
- (NSManagedObjectModel *)managedObjectModel;
- (NSManagedObjectContext *)managedObjectContext;

- (IBAction)saveAction:sender;

@end

これをベースに作って行こう。


モデルの定義

*.xcdatamodel を開いてデータモデルを定義する。エンティティ Homepage をこんな感じで作ってみた。

併せてモデルクラスを用意する。

新規ファイルを作成し、属性をプロパティ定義していく。このあたりXCodeが自動生成してくれると良いのだが(何か方法があるのだろうか?)。
Homepage.h

#import

@interface Homepage : NSManagedObject {

}
@property (retain, nonatomic) NSDate * createdDate;
@property (retain, nonatomic) NSString * imageName;
@property (retain, nonatomic) NSString * memo;
@property (retain, nonatomic) NSDate * modifiedDate;
@property (retain, nonatomic) NSString * title;
@property (retain, nonatomic) NSString * url;

@end



Homepage.m

#import "Homepage.h"

@implementation Homepage

@dynamic createdDate;
@dynamic imageName;
@dynamic memo;
@dynamic modifiedDate;
@dynamic title;
@dynamic url;

@end



@dynamic 指定している件については以前のブログを参照されたし。
Cocoaの日々: CoreData - NSManagedObject のプロパティ


テーブルビューの用意とバインディング

InterfaceBuilder を立ち上げ、ウィンドウへテーブルビューを配置する。


続いてモデルとビューの橋渡しを行う NSArrayController を MainMenu.xib へ追加する。


これを MainMenu.xib へ追加。

インスペクタを開き、Attributes を編集する。

変更点は下記の通り:
  • Mode を Entity へ変更する
  • Entity Name へ先ほど用意したモデルでのエンティティ名 Homepage を指定する
  • Prepares Content にチェックを入れる(これで NSArrayController が自動的に NSManagedObjectContext 経由で保存データを読み込んでくれる)。 

次にテーブルビューのカラムを選択した後、NSArrayController へバインドする。

Controller Key へ arrangedObjects を、Model Key Path に title(カラムに表示するモデルの属性)を指定する。

他のカラムも同様にバインドしておく。

ここまででコントローラー(NSArrayController)とビュー(NSTableColumn)とのバインドができた。後はコントローラとモデルの紐付けが必要。NSArrayController は(親クラスの NSControllerは)CoreDataをサポートしていて -[setManagedObjectContext:] がある。これを使ってコントローラとモデルを結びつける。
参照:Mac Dev Center: NSObjectController Class Reference

BlogAssistant_AppDelegate.h

@interface BlogAssistant_AppDelegate : NSObject
{
      :
    IBOutlet NSArrayController *arrayController;
}



ヘッダへアウトレットを一つ追加し、InterfaceBuilderでNSArrayController へ接続する。

最後に NSArrayController へ NSManagedObjectContext を設定する。これは今回コードで書く。

BlogAssistant_AppDelegate.m

- (void)awakeFromNib
{
[arrayController setManagedObjectContext:[self managedObjectContext]];

}



これで Model-View-Controller が全てつながった。データの管理は NSManagedObjectContext が良きに?取りはからってくれる。


テスト用ボタン追加

表示を確認するにはテストデータが必要。テストデータを追加するボタンを付ける。

ボタンのターゲットは BlogAssistant_AppDelegate とし、アクションは addTestRecord: につなぐ。実装はこんな感じ。

BlogAssistant_AppDelegate.m

- (IBAction)addTestRecord:(id)sender
{
Homepage* homepage = [NSEntityDescription insertNewObjectForEntityForName:@"Homepage"
inManagedObjectContext:[self managedObjectContext]];
homepage.title = [NSString stringWithFormat:@"TEST TITLE:%@", [[NSDate date] description]];
homepage.imageName = @"TEST IMAGE";
homepage.url = @"http://xcatsan.com/";
homepage.createdDate = [NSDate date];
homepage.modifiedDate = [NSDate date];
homepage.memo = @"MEMO";

NSError* error = nil;
[[self managedObjectContext] save:&error];

if (error) {
NSLog(@"INSERT ERROR: %@", error);
} else {
NSLog(@"INSERTED");
}

}


保存形式を SQLite へ変更する

自動生成されたコードでは保存形式が XMLになっている。これを SQLへ変更する。


BlogAssistant_AppDelegate.m

- (NSPersistentStoreCoordinator *) persistentStoreCoordinator {

    url = [NSURL fileURLWithPath: [applicationSupportFolder stringByAppendingPathComponent: @"BlogAssistant.db"]];
    persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
    if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error]){
        [[NSApplication sharedApplication] presentError:error];
    }  


}



動作確認

動かしてみよう。起動して何度かボタンを押してテストデータを生成する。


NSManagedObjectContext へ対するテストデータ追加がテーブルビューへ自動的に反映された。ダブルクリックすると変更もできる。

アプリを止めて再起動するとデータが復元される。

ここまでで MVCの連携コードを書いて InterfaceBuilder を設定しただけ。CoreDataとBindingsを使うと、ローカルDB付きのテーブルビューがこんなに簡単にできてしまう。


保存データ

さてデータはいったいどこに保存されているのか。自動生成されたコードを見るとライブラリ配下の Application Support にあるようだ。

    url = [NSURL fileURLWithPath: [applicationSupportFolder stringByAppendingPathComponent: @"BlogAssistant.db"]];



ターミナルを開き sqlite3 コマンドで中身が確認できる。




ソースコード

github へ上げました。
BlogAssistant at master from xcatsan's SampleCode - GitHub



おまけ:SQLite のログ

実行時オプションにを -com.apple.CoreData.SQLDebug 1 付けるとデバッガコンソールへ SQLite のログを出力させることができる。

(参考)Cocoa Touch の日々: CoreData で発行されている SQL をデバッグ出力する

こんな感じ。


- - - -
BlogAssistant開発:続く..