Cocoa

フィルタアニメーションのやり方がわからない...

テキストフィールドにフィルタをかけたるアニメーションを実装したいと思ったのですがなかなか思う通りにできません。みるくCocoaさんのサンプルにまさにコレ!というのがあったので早速試したのですが、デフォ0.25秒の動きが早すぎるのかどうしてもアニメーションしているようには見えませんでした。本家アップルののように、

CABasicAnimation *theAnimation =
  [CABasicAnimation animationWithKeyPath:@"filters.myGussian.inputRadius"];

theAnimation.duration            = 10;
theAnimation.fromValue           = [NSNumber numberWithFloat:5.0];
theAnimation.toValue             = [NSNumber numberWithFloat:0.0];
  
[[answerField layer] addAnimation:theAnimation forKey:@"filterAnimation"];

のように書き直しても動く気配なし。そもそもCore ImageとかCore Animationとか全然わかっていないのでコードの断片だけを拾い集めてきてもうまくいくはずもなく、アニメーションはあきらめて

if ([text wantsLayer])
{
  [text setWantsLayer:NO];
  [text setContentFilters:nil];
  return;
}
CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur" keysAndValues:
                   kCIInputRadiusKey, [NSNumber numberWithFloat:2.5], nil];
[text setContentFilters:[NSArray arrayWithObject:filter]];
[text setWantsLayer:YES];

として、フィルタの適用をオン・オフだけするだけの実装にしてごまかして(実際これだけでも十分なんですが...)Core mage/Core Animation周りを勉強し直すことにしました。というわけで、これをポチ。

Core Animation for Max OS X and the iPhone: Creating Compelling Dynamic User Interfaces (Pragmatic Programmers) Core Animation for Max OS X and the iPhone: Creating Compelling Dynamic User Interfaces (Pragmatic Programmers)
Bill Dudney

Pragmatic Bookshelf 2008-10-15
売り上げランキング : 17174

Amazonで詳しく見る by G-Tools

アイコンポップアップボタン

in

Snow Leopardのアカウントアイコンの選択や、バージョンアップしたTarnsmit4にアイコンを選択するポップアップメニュー風のUIがあるのですが、

 Icon PopupMenu

子ノードには手を出すな(NSTreeController)

in

このところまたOutlineView+TreeControllerやってます。それでひとつ気づいたことをメモ。

"ツリーコントローラーにストアしているデータをすべてinsertしてもツリー構造にはならない"ということ。読み込んだ子ノードのデータをTreeControllerにinsertすると(親ノードの配下にも表示されますが)親ノードと兄弟のように表示されちゃいますね。ググってみたら、Fetch Predicateに”parent == nil"を指定する(cocoadev:NSTreeController)のが慣例みたいなんですが知りませでした。データの読み込みを確認しながらようやくわかった次第。要するに親ノードが持っている子ノードの展開はTreeControllerが内部で実行することになっているようでプログラムからは親ノードさえadd/insertしてやれば良いようです。当然バインドの場合でも上記の例のようにPredicateを指定する必要があるようです。(Appleの公式ドキュメントとしてどこかに書かれているのかな?)

NSManagedObjectContext *moc = [self managedObjectContext];
NSFetchRequest *fetch = [[[NSFetchRequest alloc] init] autorelease];
NSArray* sortDesc = [NSArray arrayWithObject:
  [[[NSSortDescriptor alloc] initWithKey:@"orderNo" ascending:YES] autorelease]];

[fetch setEntity:
  [NSEntityDescription entityForName:@"ChildNodeEntity" inManagedObjectContext:moc]];
[fetch setPredicate:[NSPredicate predicateWithFormat:@"isLeaf == 0"]];
[fetch setSortDescriptors:[self sortDesc]];

NSError *err     = nil;
NSArray *results = [moc executeFetchRequest:fetch error:&err];
if ((err) || (results == nil))
{
  [NSApp presentError:err];
  return;
}

NSUInteger index = 0;

for (ChildNodeEntity *node in results)
{
  [treeController insertObject:node atArrangedObjectIndexPath:
    [NSIndexPath indexPathWithIndex:index]];
  index = index + 1;
}

まあ、実際にやりたかったのはノードの表示順を一定に保つことでCocoaDevのリンクnstreecontroller-and-core-data-sortedにもあったようにノードの中になんらかの方法で順序を示す数値を仕込んでおけばOK。子ノードの展開はTreeControllerお任せになるのでSortDescriptorをバインドする必要もあります。ロード時に特別の処理がなければ僕のコードのようにガリガリコードを書くのは不作法で、やっぱりバインドしたほうがスマートですね。

EVP_*系APIでのBlowFish実装

パスワードを保存するアプリを作るため以前使ったことのあるBlowFishのmanを読み直していたら、BF_*系の関数を直接使うんじゃなくてもっと高レベルのEVP_EncryptInitとかを使いなよって書いてあったので忠告に従いこちらを使うべく調べてみました。BF_*系と違うのは暗号・復号に際して必要な情報を追跡するためのコンテキスト情報EVP_CIPHER_CTXを維持していく必要があるのが大きな違いになります。処理は

  1. 暗号コンテキストの初期化
  2. 暗号化方式の決定と初期化
  3. 暗号・復号処理
  4. 暗号コンテキストの解放

といったフローでそれほどステップは多くないカンジ。まず、暗号コンテキストの初期化は

Core Dataのマイグレーション(手動編)

in

既存のマッピングで対応できないような場合、NSEntityMigrationPolicyを拡張してカスタマイズできることはわかったのですがドキュメントを読むだけではいまひとつピンとこなかったので実際にやってみました。トライしたのは下記のアドレス帳モデルの変換。とりあえず、v1のモデルで固定・携帯の2つの電話を持つ桃太郎、携帯電話のみを持つ浦島太郎、電話は持たない一寸法師のアドレス帳データを作成しこれをv2モデルに変換します。マイグレーションの骨子は、

  1. 電話を持たない人はそのまま新しいAddressBookエンティティにマッピング
  2. 電話を持つ人はPhone1/Phone2をPhoneBookエンティティにマッピング

の2点。1に関しては通常のマッピングで対応し、2に関してマイグレーションポリシーを拡張します。

まず、v1のAddressBookエンティティにあるphone1とphone2をPhoneBookエンティティに移行するため新しいマッピングを適当な名前で作成します。ここではAddressBookToPhoneBookという名前のマッピングモデルを作成しました。ソースにはAddressBook、デスティネーションにはPhoneBookを指定します。また、電話を持たないAddressBookインスタンスはPhoneBookへのマッピングは不要なのでフィルタ述語に

Core Dataの自動マイグレーション

in

Core Dataの使い始めの頃、マイグレーションには散々苦労したのですがいつの間にか自動マイグレーション機能なんかができていてこれがいたって簡単でした。手順としては

  1. 新しいモデルの作成
  2. マッピングモデルの作成
  3. 自動マイグレーションの指定

の3ステップだけ。

まず新しいモデルを作成する場合ですが、既存のモデルエディタに直接修正を加えるのではなくて、[設計]-[データモデル]-[モデルバージョンを追加]で新しいモデルを作成します。これで、既存のモデルをベースに新しいモデルが作成されますので新しいモデルに必要な修正を加えます。モデルの作成が終了したら[設計]-[データモデル]-[現在のバージョンを設定]で、新しく作成したモデルをカレントモデルに設定します。これが第一段階。

次はマッピングモデルの作成。ある意味、これがマイグレーションの本体となります。[新規ファイル追加]-[リソース]-[マッピングモデル]で新しいマッピングモデルを作成しますが、ここで「ソースモデル」と「デスティネーションモデル」を指定します。「ソースモデル」が既存のモデル、「デスティネーションモデル」が先に作った新しいモデルファイルになります。

このマッピングモデルに(旧)エンティティ(のどのプロパティ)が(新)エンティティ(のプロパティ)にマッピングされるかを記述していくわけです。作成直後の状態では、名前を元にマッピングが作成されているのでこれをベースに必要なマッピングを記述していけばホント簡単にマイグレーションできちゃいます。マッピングは新旧エンティティが一対一にならなくとも良くて、いくつもマッピングを作成することができます。例えばPredicateを指定すれば、ある(旧)エンティティAのうち、条件xに合致するデータは(新)エンティティA'にマッピング、条件yに合致するデータは(新)エンティティA''にマッピングなどということができるわけです。

最後は、コーディネーターに自動でやってねってお願いするのを忘れずに。

NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
    [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
[persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType 
                            configuration:nil 
                            URL:url
                            options:options
                            error:&err]);

たったこれだけで、エンティティとその属性(もちろん関連も)移行してしかも殆どノーコード。モデルのバリデーション(例えば新しく追加した属性がオプショナルでない場合など)に注意すれば実にスマートな移行ができます。実際のところ、単なる属性追加だけとか非オプションをオプションに変えるだけとかだったらマッピングモデルの作成すら省略することができるなんてサービスしすぎな気がしなくもないです。

さらにはNSEntityMigrationPolicyをオーバーライドしてさらに複雑なマイグレーションのための手段もあるので次回はもう少し複雑なパターンを書いてみようと思ってます。

あっ、v10.4では...ちょっとね、ダメみたいです。悲しいことに。

(参考)Introduction to Core Data Model Versioning and Data Migration Programming Guide

NSTaskは~/Library/Apprication\ SupportでもOK

in

NSTaskでスクリプトを実行する時、${HOME}/Libray/Apprication\ Supportでも実行できるか不安だったので念のため試してみたところちゃんと実行できました。いや、単にパス名にスペースが含まれているから実行できるか不安だっただけですが...(MacOSXには"Program\ Files"的なものはないかと思っていましたがあるんですね。)

- (void)setCurrentDirectoryPath:(NSString *)pathで移動してやればOK。Cocoaはやっぱり! - Perlスクリプトを実行するに詳しいです。これまでtemporaryディレクトリなんかを使ってやっていたんですが、ここを使えば競合なんかの心配が限定できるのでじゃかじゃかやれます。Core Dataのデフォルト保存パスがここになっていることでようやくこのテに気がつきました。日本語を含むディレクトリでも正しく起動できました。ま、カレントディレクトリを変えちゃうんだから当たり前なんだけど、Winじゃ"Program\ Files"とか"My\ Document"とか痛いことあるし...

ガベージコレクションに気をつけて

in

先日作成したftpのサンプルアプリですがNSInputStream/NSOutputStreamのretain/releaseを書いてないのでGCオプションをなしにするとエラーとなります。自分ではこのオプションを変更した記憶はないのですが、なにせv10.4対応アプリを作成中なので色々いじっているうちに、なぜかDebugバージョンは[GCC_ENABLE_OBJC_GC, -fobjc-gc-only]で、Releaseバージョンでは非対応になってしまったようです。せめて逆なら良かったのですが、こういうオプションは構成毎に連動してくれればいいのに。と思ったりもしますがもちろんそれくらい自分で管理しないとダメ。とにかく、これにハマると、Bad AccessやらSelectorがないよ!とかいろんなエラーが出て、迷いに迷うので要注意です。

Cocoaでアプリケーション名をローカライズするには...

in

Cocoaでリソース関連をローカライズするにはIBで日本語なら日本語のリソースを作ってやればいいのだけれどアプリケーションの名称はこれではローカライズできません。

スクリーンショット

CocoaでFTPする方法

in

FTPのput機能が必要になる要件があってConnectionKitを使うツモリで調べてたんですがこのFrameworkのサンプルがビルドできなくてアレだったし、Applのサンプルをみたら意外と簡単そうだったので自前で実装してみました。

  1. putするローカルファイルのNSInputStreamを用意する
  2. リモート側に出力するためのNSOutputStreamを用意する
  3. RunLoopのdelegateメソッドを実装する
  4. ストリームの後始末

といった手順になります。

NSURLのエスケープ指定

in

clippyのエイリアスに日本語ファイル名を指定すると読み込めないというバグがありました。自分では滅多に日本語ファイル名などというのは使わないので全然ノーマーク。

NSString:stringWithContentsOfURL:encoding:errorを使って読み込みする際、読み込むファイルのNSURLを単純に生成してました。

NSString *contents = 
  [NSString stringWithContentsOfURL:[NSURL URLWithString:filename] 
  encoding:NSUTF8StringEncoding error:&err];

これだとNSURL:URLWithStringはnilを返しますが、なんのチェックもなしです。(^^ゞなんでもかでも一行に納めるとこういう場合がきつい。ちゃんと2ステップ踏んで、fileスキームもちゃんとエスケープしないとダメですね。

NSURL    *url = [NSURL URLWithString:
  [filename  stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSString *contents = 
  [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&err];

便利なものがあって助かりました。しかし、どうせならエスケープするというのをデフォルトにすればいいのになぁ。などと少し思ったりもします。

すべてのウィンドウ情報を取得する方法。Scripting Bridge vs AX*

in

OSXである時点の表示されているすべてのウィンドウ位置を取得するのはScripting Bridgeで簡単にできると思っていたのですがいざやってみたらうまくできませんでした。やり方は、起動中のすべてのプロセス取得。そのプロセスが持っているウィンドウ情報を引き出すという手法でできると思ったですが...

まず、起動中のプロセス取得。これはシステムイベントから取得できます。

  SystemEventsApplication *systemEvents   = 
  [SBApplication applicationWithBundleIdentifier:@"com.apple.systemevents"];
  SBElementArray *procs = [systemEvents processes];

でこのプロセスからウィンドウ情報を取得します。

for(SystemEventsProcess* proc in procs)
{
   for (SystemEventsWindow *w in [proc windows])
   {
      NSLog(@"%@ :%@ (%.1f:%.1f,%.1f:%.1f),%d,%@,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d",
            [proc name],
            [w name],
            w.bounds.origin.x, w.bounds.origin.y,
            w.bounds.size.width,w.bounds.size.height,
            w.closeable,
            [[w document] name],
            w.floating,
            w.id,
            w.index,
            w.miniaturizable,
            w.miniaturized,
            w.modal,
            w.resizable,
            w.titled,
            w.visible,
            w.zoomable,
            w.zoomed
         );
     }
  }

これで完璧だと思ったのですが、取得できたのはw.nameだけ。あとは皆NULLかゼロという悲惨な状況でした。プロパティはあるのになんで!

CocoaとC#のデリゲート

以前作成したアプリをv10.6SDKベースでコンパイルしたら警告がでました。何でかなーと思って調べてみるとアプリケーションで使用しているNSRuleEditorのdelegateメソッドがNSRuleEditorDelegateプロトコルに昇格(?)していました。なので、delegateメソッドを実装するクラスは<NSRuleEditorDelegate>しなければいけませんでした。ドキュメント10.6 Symbole Changesを見てみるとNSWindowはじめ多くのdelegateがプロトコル化されていることがわかります。

Objective-C 2.0でプロトコルでもオプション指定ができるようになったので、カテゴリ(非形式プロトコル)定義されていたデリゲートが形式プロトコル化されるのは自然な流れかもしれません。これまではよほど大量のデリゲートメソッドを実装するのでなければ、コントローラクラスの片隅にチョコっとデリゲートメソッド実装していましたが、オブジェクトの独立性や保守性を考えれば、今後はきちんとデリゲート用のクラスを作成するという方向になっていくのでしょう。

デリゲートといえば、前エントリでは嬉々としてデリゲート便利!などと騒いだ手前恥ずかしいのですが、冷静になってみるとInvoke(delegate)はやっぱり単なる「関数ポインタ」と言ったほうがいいのじゃないかと思ったりしてます。設計上の理由によりメソッドの実装を”他オブジェクトに譲(ってもいい|るべきだ)”とすることをデリゲートと呼ぶのであって、たとえばコールバックでの関数ポインタの代替として(文法上)のデリゲートとは意識的に分けて考えたほうが正しいあり方のように思います。

鏡の国では不思議がたくさん

comp.lang.objective-cのConfusion with NSMutableStringのトピックに面白い(表現の)トピックがありました。お題は、以下のコードで、

NSString* path = @"~";
path = [path stringByExpandingTiledInPath];

pathが変更できるのは変じゃない?というものなのですが、それに対して

You haven't understood "Through the Looking Glass" :-)

というリプライがあって何か特別な慣用句なのかショボイ英語力の僕には知れませんが、言い得て妙な表現です。英語圏の人にはこれで「あっ!」って感じで腑に落ちるんでしょうかね。

そういえば、Objctive-Cを始めた頃、こんなコードを書いてしまったことがありましたがこれもhaven't understood "Through the Looking Glass"ですね。

Foo* foo = [Foo alloc];
[foo init];

チェシャ猫は何も言ってくれませんでした(*_*)。鏡の国は、不思議の国...です。

NSRuleEditorのローカライズ(その2)

前回のエントリで書いたようにNSRurleEditorではルールを作成に際してDelegateメソッドを通じて細かい制御をおこなうことができます。必須のDelegeteメソッドは3つ、

  1. ruleEditor:numberOfChildrenForCriterion:withRowType:
  2. ruleEditor:child:forCriterion:withRowType:
  3. ruleEditor:displayValueForCriterion:inRow:

NSRuleEditorのaddRowメソッド呼び出しのタイミングで左辺・述部・右辺についてそれぞれ呼び出されます。今回のサンプルで言えば初回時(ルート)では price/color/sizeの3つの選択肢があるので返す数は3。NSRuleEditorではこの数に基づいて、ruleEditor:child:forCriterion:withRowType:メソッドが3回呼び出します。このメソッドでは左辺3つの情報をNSDictionary形式にパックして返します。パラメータchild:(NSInteger)indexで表示位置を指定してきますのでindex(0)でpriceの情報を、index(1)でcolorの情報・index(2)でsizeの情報を返します。

コンテンツの配信