ページ

2010年2月7日日曜日

今週のCocoa情報(2/7) - 今週気になった Cocoaプログラミング情報の紹介

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


Objective-C: What's the Difference between objectForKey and valueForKey? - Stack Overflow

objectForKey: と valueForKey: の違いは?
前者は NSDictionaryメソッド、後者は KVCメソッド。objectForKey: は文字列以外もキーにできる。など。

関連:
valueForKey: on collections [was: Re: Using isMemberOfClass with a tree of subclass of NSManagedObject]



How is release handled for @synthesized retain properties? - Stack Overflow

@proprty / @synthesize 宣言したプロパティの relelase は自前ですべきかどうか(答え:すべき)。



How do you do TDD in Xcode? - Stack Overflow

TDD開発に使えるフレームワークは?

Unit Test に関して紹介されていたリンク
Mac Dev Center: Xcode Unit Testing Guide: Introduction
- SenTestingKit(OCUnit) を使った unit test。

Chris Hanson - Xcode: Unit Testing Series
- OCUnit の使い方など

ASIHTTPRequest Documentation - All-Seeing Interactive
- CFNetwork のラッパー/Webアクセスライブラリ。これ自体はテストフレームワークではない。脇道にそれるが、このフレームワークはなかなか機能豊富で良さそう。以下、引用。


ASIHTTPRequest is an easy to use wrapper around the CFNetwork API that makes some of the more tedious aspects of communicating with web servers easier. It is written in Objective-C and works in both Mac OS X and iPhone applications.
It is suitable performing basic HTTP requests and interacting with REST-based services (GET / POST / PUT / DELETE). The included ASIFormDataRequest subclass makes it easy to submit POST data and files using multipart/form-data.


It provides:
  • A straightforward interface for submitting data to and fetching data from webservers
  • Download data to memory or directly to a file on disk
  • The ability to submit files on local drives as part of POST data, compatible with the HTML file input mechanism
  • Easy access to request and response HTTP headers
  • Progress delegates (NSProgressIndicators and UIProgressViews) to show information about download AND upload progress
  • Auto-magic management of upload and download progress indicators for operation queues
  • BasicDigest and NTLM authentication support, credentials are automatically for the duration of a session, and can be stored for later in the Keychain.
  • Cookie support
  • GZIP support for response data AND request bodies
  • Experimental support for Amazon S3
  • Supports manual and auto-detected proxies, authenticating proxies, and PAC file auto-configuration. The built-in login dialog lets your iPhone application work transparently with authenticating proxies without any additional effort.
  • Bandwidth throttling support
  • NEW Support for persistent connections
  • Supports synchronous & asynchronous connections, requests can run in the main thread, or in an NSOperationQueue
  • Comes with a broad range of unit tests
GZIP や S3 をサポートしているのがユニーク。iPhone でも使えるようだ。
ASIHTTPRequest is compatible with Mac OS 10.5 or later, and iPhone OS 2.2.1 or later.

gabriel's gh-unit at master - GitHub
- ASIHPRequest が使っている GHUnit。

その他参考リンク:
Test Driving Your Code with OCUnit



iPhone用サーバーアプリは ObjectiveResource + Ruby on Rails で超簡単!! - yuum3のお仕事日記

Ruby on Rails を使った iPhone連携サイト構築のライブラリ。連携用に Objective-Cライブラリも用意されている。



ポッチンコンバート:ポッチンルーム

ドキュメントの日本語訳。少し古いがざっと目を通すにはいい。





Mac Dev Center: File System Overview: Introduction to the File System Overview

Mac OS X ファイルシステムの基礎の基礎。いまさらながら存在に気がついた。一通り目を通しておきたい。

2010年2月6日土曜日

アプリケーション名とフォルダ名のローカライズ(その1)

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

アプリケーション名やフォルダ名をローカライズするにはどうしたらいいのか?

(例)Preview.app は日本語環境のファインダで見ると「プレビュー」となる。



Display Names
アプリ名とフォルダ名のローカライズに関しては Mac Dev Center の "Display Names" というドキュメントに記載がある。

Mac Dev Center: File System Overview: Display Names

これによれば MacOSX では下記をサポートしているとのこと。

・アプリケーション名とフォルダ名を多言語化できる
・ファイル名の拡張子を隠せる

以下、ガイドラインのまとめ。

  • 実際の名前の他、表示用の名前を用意することができる(これを Display Name / 表示名と呼ぶ)。
  • 表示名はプリファレンスの言語設定ごとにつけることができる。(例)Pictures フォルダは、ドイツ語で Bilder、日本語で ピクチャ、と表示される。
  • 表示名は読み取り専用の文字列として扱う。変更できるようにすべきではない。
  • 表示名は一時的な情報と考え、それをデータとして保存したりすべきではない。必要な場合は実際の名前を使う。
  • 表示名はファインダと Open and Save ダイアログで使われる。
  • コマンドラインアプリケーションでは表示名を使うべきでない。


表示名(Display Names)の取得の仕方

・Carbonアプリなら LSCopyDisplayNameForURL を使う。
・Cocoaアプリなら -[NSFileManager displayNameAtPath:] を使う。
Mac Dev Center: NSFileManager Class Reference


アプリケーション名のローカライズ
このドキュメントによれば InfoPlist.strings に CFBundleDisplayName を定義すれば良いとのこと。サンプルアプリを作って実際に試してみる。

Xcodeテンプレートでアプリを1つ作る。その中の InfoPlist.strings を選びローカライズする。

まず「情報を見る」を選ぶ。
「ローカリゼーションを追加」で "Japanese" を追加する。
作成された InfoPlist.strings/Japanese の情報を開き、エンコーディングを UTF-16 に変えておく。
InfoPlist.strings/Japanese に CFBundleDisplayName を追加する。

/* Localized versions of Info.plist keys */

CFBundleDisplayName = "表示名サンプル";


ビルドしてファイルを見てみる。が、アプリ名が変わらなかった。
ドキュメントに書かれていた LSHasLocalizedDisplayName を Info.plist へ追加してみる。

Info.plist の Key へ書き込むと変換されて "Application has localized display name" に変わった。

さてビルドしてみよう。
アプリケーション名が変わった。

実行すると Dock上でもちゃんと日本語になっている。


ターミナルでみると DisplayNamesSample.app となっている。

ちなみに InfoPlist.strings/English では次のように設定してある。

/* Localized versions of Info.plist keys */

CFBundleDisplayName = "Sample";


つまり英語だけが特別扱いにならないということ(当たり前か)。

アプリケーションの実体の名前はどこで決まるのか?Info.plist ファイルにありそうだが ${PRODUCT_NAME} としか書かれていない。
実はターゲットのビルド設定にある。

ターゲットの情報を開き、

「パッケージング」の中の「プロダクト名」を探す。
ここにあった。試しに変えてみよう。試しに "Renamed-Product" としてみた。

ビルドすると

変わった。

 - - - -
次回はフォルダ名のローカライズを調べる(本当はそっちが最初の目的だった)。

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月3日水曜日

KVC - key の存在チェック

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

KVC (Key Value Coding) は便利だが、あるオブジェクトが指定の keyを持っていることを確認するにはどうすればいいのか?

例えば Book というクラスがある時
Book.h

@interface Book : NSObject {

 NSString* author;
 NSString* title;
}
@property (retain) NSString* author;
@property (retain) NSString* title;

@end

実行時に Book が "author" というkeyを持つかどうか知りたい。あるいは "weight" を持っているのかどうか。

NSKeyValueCoding Protocol にはそれらしいメソッドが見当たらなかった。
Mac Dev Center: NSKeyValueCoding Protocol Reference

Mac Dev Center のドキュメントを探していくと keyが指定された時の探索方法の解説があった。
Mac Dev Center: Key-Value Coding Programming Guide: Accessor Search Implementation Details

これによると例えば setValue:ForKey: をあるオブジェクトへ投げたときに、当該keyが存在しない場合は setValue:forUndefinedKey: が投げられるとある。
4. If no appropriate accessor or instance variable is found, setValue:forUndefinedKey: is invoked for the receiver.
この setValue:forUndefinedKey: を見るとデフォルトでは例外(NSUndefinedKeyException)が発生するとのこと。
Mac Dev Center: NSKeyValueCoding Protocol Reference

Discussion
Subclasses can override this method to handle the request in some other way. The default implementation raises an NSUndefinedKeyException.

ネットで調べてみると Stack Overflow にこの内容の質問が出ていた。
How do you tell if a key exists for an object using Key-Value Coding? - Stack Overflow

提示されていた方法をまとめると次の通り。

  1. 例外をキャッチして処理を続行 @try-@catch
  2. valueForUndefinedKey: を実装(セッターの場合は setValue:forUndefinedKey:)
  3. class_copyPropertyList( ) でプロパティリストを取得してチェック
  4. respondsToSelector: でアクセッサ実装の有無をチェック
なるほど。

コードを書いて挙動を確認してみた。先程の Book クラスに対して試してみる。


Book* book = [[Book alloc] init];


[book setValue:@"APPLE" forKey:@"author"];
[book setValue:@"iPad book" forKey:@"title"];


ここまではいい。


[book setValue:@"a value" forKey:@"test"];


"test" は存在しないので例外が発生。

[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key test.



ちなみにオブジェクトが NSManagedObject のサブクラスの場合でも上記例外が発生した(通常のクラスと変わらない)。



undfined 系メソッドを実装する。

@implementation Book

@synthesize author;
@synthesize title;

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
// do nothing
}

- (id)valueForUndefinedKey:(NSString *)key
{
return nil;
}
@end


すると例外が消えた。


次に respondsToSelector:を試す(undefined 系メソッドを一旦コメントアウトしておく)。

if ([book respondsToSelector:NSSelectorFromString(@"test")]) {
NSLog(@"ok: test");
[book setValue:@"a value" forKey:@"test"];
}

if ([book respondsToSelector:NSSelectorFromString(@"author")]) {
NSLog(@"ok: author");
}


"ok: author" のみ表示された。

- - - -
どの方法を取るかは状況によりそうだ。

なお下記のように undefined系メソッドをカテゴリで実装しても有効に働いた。

@implementation Book (Addition)

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
// do nothing
}

- (id)valueForUndefinedKey:(NSString *)key
{
return nil;
}

@end

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年2月1日月曜日

@property の一覧を取得する

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

例えばこんなクラスがあった時にプロパティの一覧を取得するにはどうするか?

@interface Book : NSObject {

 NSString* title;
 NSString* author;
 NSDate* createdDate;
 float weight;
}
@property (retain) NSString* title;
@property (retain) NSString* author;
@property (retain) NSDate* createdDate;
@property float weight;
@end

以前紹介した StatckOverflow の記事を参考にコードを書いてみた。

How do I find all the property keys of a KVC compliant Objective-C object? - Stack Overflow

ランタイム関数 class_copyPropertyList( ) を使う。こんな感じ。
#import <objc/runtime.h>
#import "AppController.h"
#import "Book.h"

@implementation AppController

-(void)awakeFromNib
{
 unsigned int outCount, i;
 objc_property_t *properties = class_copyPropertyList([Book class], &outCount);

 for(i = 0; i < outCount; i++) {
  objc_property_t property = properties[i];
  const char *propName = property_getName(property);
  const char *propType = property_getAttributes(property);
  NSString *propertyName = [NSString stringWithUTF8String:propName];
  NSString *propertyType = [NSString stringWithUTF8String:propType];
  NSLog(@"%@: %@", propertyName, propertyType);
 }
 free(properties);
}

@end

実行結果

2010-02-01 21:08:02.470 GetProperty[6929:10b] weight: Tf,Vweight
2010-02-01 21:08:02.475 GetProperty[6929:10b] createdDate: T@"NSDate",&,VcreatedDate
2010-02-01 21:08:02.478 GetProperty[6929:10b] author: T@"NSString",&,Vauthor
2010-02-01 21:08:02.480 GetProperty[6929:10b] title: T@"NSString",&,Vtitle


property_getAttributes の記号の意味はリファレンスに詳しく書かれている。様々な例が網羅されているの役立つ。

Mac Dev Center: Objective-C Runtime Programming Guide: Declared Properties


参考:
Mac Dev Center: Objective-C Runtime Reference - class_copyPropertyList


サンプルのソース
GitHubからどうぞ

GetProperty at master from xcatsan's SampleCode - GitHub