ネイリスト向けアプリ hair Concierge(ネイル・コンシェルジェ)が公開されました

2010年12月31日金曜日 | Published in | 0 コメント

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

ネイリスト向け顧客管理 iOS 向けアプリ「hair Concierge(ネイル・コンシェルジェ)」が AppStore で公開されました。

先日公開された「hair Concierge(ネイル・コンシェルジェ)」の姉妹版となります。今回もプログラミングで参加しています。以下技術解説。


アーキテクチャ


プログラムのほとんどは前回リリースの hair Concierge と同じものを使っている。大きく違うのはマスタデータとデザイン。その他、一部のコードでそれぞれ独自の処理が入っている箇所がある。これらを Xcode に複数のターゲットを導入して一つのプロジェクトで管理するようにした。
ターゲットを切り替えることで作成するアプリを切り替えている。

デザインは構造やサイズは同じだがそれぞれのベースカラーに合わせて変えてある。これらを管理する為に、それぞれのターゲット用に Resourcesグループを用意し、実フォルダもそれぞれ作成してある。
画像ファイルのターゲットはこんな感じ。これでターゲット毎に使用する画像を切替えられる。
一部異なるインターフェイスを持つ部分は同名の Nibファイルをそれぞれ用意し、画像と同じくフォルダとターゲットを分けて管理する。


注意点など


2つのアプリを1つのプロジェクトで管理するにあたり、ファイルの名前を変えたりしたのだがそれが原因でいくつかのトラブルが発生した。詳細については以前紹介したのでそちらを参照のこと。

Cocoaの日々: [iOS] プロダクト名を変えてはいけない
Cocoaの日々: [iOS][Mac] CoreData - マイグレーション[4] モデルファイルの構成


参考情報


Cocoaの日々: 美容師向けアプリ hair Concierge(ヘア・コンシェルジェ)の技術解説


- - - - -
ブログ更新がなんとか年内に間に合った。それでは良いお年を!

[Mac][iOS] GDB - infoコマンド

2010年12月30日木曜日 | Published in | 0 コメント

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

デバッグ中に使える info コマンドで使えるいくつかの便利なものについて紹介する。

info


デバッガコンソールにプロンプト "(gdb)" が出ている状態で info と打ち込むと利用可能なサブコマンドの一覧が表示される。
"info" must be followed by the name of an info command.
List of info subcommands:

info address -- Describe where symbol SYM is stored
info all-registers -- List of all registers and their contents
info args -- Argument variables of current stack frame
info auxv -- Display the inferior's auxiliary vector
info breakpoints -- Status of user-settable breakpoints
info catch -- Exceptions that can be caught in the current stack frame
info checkpoints -- Help
info classes -- All Objective-C classes
info common -- Print out the values contained in a Fortran COMMON block
info copying -- Conditions for redistributing copies of GDB
info dcache -- Print information on the dcache performance
info display -- Expressions to display when program stops
info extensions -- All filename extensions associated with a source language
info files -- Names of targets and files being debugged
info float -- Print the status of the floating point unit
info fork -- Help
info frame -- All about selected stack frame
info functions -- All function names
info gc-references -- List the garbage collectors references for a given address
info gc-roots -- List the garbage collector's shortest unique roots to a given address
info handle -- What debugger does when program gets various signals
info interpreters -- List the interpreters currently available in gdb
info line -- Core addresses of the code for a source line
info locals -- Local variables of current stack frame
info mach-port -- Get info on a specific port
info mach-ports -- Get list of ports in a task
info mach-region -- Get information on mach region at given address
info mach-regions -- Get information on all mach region for the current inferior
info mach-task -- Get info on a specific task
info mach-tasks -- Get list of tasks in system
info mach-thread -- Get info on a specific thread
info mach-threads -- Get list of threads in a task
info macro -- Show the definition of MACRO
info malloc-history -- List the stack(s) where malloc or free occurred for the address
info mem -- Memory region attributes
info pid -- Process ID of the program
info plugins -- Show current plug-ins state
info program -- Execution status of the program
info registers -- List of integer registers and their contents
info scope -- List the variables local to a scope
info selectors -- All Objective-C selectors
info set -- Show all GDB settings
info sharedlibrary -- Generic command for shlib information
info signals -- What debugger does when program gets various signals
info source -- Information about the current source file
info sources -- Source files in the program
info stack -- Backtrace of the stack
info symbol -- Describe what symbol is at location ADDR
info target -- Names of targets and files being debugged
info task -- Get information on task
info terminal -- Print inferior's saved terminal status
info thread -- Get information on thread
info threads -- IDs of currently known threads
info tracepoints -- Status of tracepoints
info trampoline -- Resolve function for DYLD trampoline stub and/or Objective-C call
info types -- All type names
info variables -- All global and static variable names
info vector -- Print the status of the vector unit
info warranty -- Various kinds of warranty you do not have
info watchpoints -- Synonym for ``info breakpoints''


info args


実行中の関数の引数情報が表示される。
(gdb) info args
self = (CustomArrayController *) 0x10061b8f0
_cmd = (struct objc_selector *) 0x100003487
objects = (NSArray *) 0x10064ab90


info malloc-history


指定したアドレスを持つオブジェクトの malloc/free された経緯(スタック)を表示する。
(gdb) info malloc-history 0x10064ab90
Alloc: Block address: 0x000000010064ab90 length: 56
Stack - pthread: 0x7fff70913ca0 number of frames: 27
    0: 0x7fff85b8ef0e in malloc_zone_malloc
    1: 0x7fff83e15e83 in __CFBasicHashRehash
    2: 0x7fff83e21e8d in __CFBasicHashAddValue
    3: 0x7fff83e29698 in CFBasicHashSetValue
    4: 0x7fff83e294b7 in CFDictionarySetValue
    5: 0x7fff849fb00a in -[NSButtonCell _coreUIBezelDrawOptionsWithFrame:inView:]
    6: 0x7fff849fa518 in -[NSButtonCell drawBezelWithFrame:inView:]
    7: 0x7fff849dfda7 in -[NSButtonCell drawWithFrame:inView:]
    8: 0x7fff849d83c8 in -[NSControl drawRect:]
    9: 0x7fff849d0c49 in -[NSView _drawRect:clip:]
   10: 0x7fff849cf8bc in -[NSView _recursiveDisplayAllDirtyWithLockFocus:visRect:]
     :

開放済みのオブジェクトへメッセージを送って落ちた時などの原因究明に使える。
-[NSSortDescriptor count]: message sent to deallocated instance 0x1f5480

なおこのコマンドを利用するには予め環境変数 MallocStackLoggingNoCompact を設定する必要がある。未設定の状態で実行すると下記のメッセ次が表示される。
(gdb) info malloc-history 0x100557600
warning: MallocStackLoggingNoCompact not set in target's environment so the malloc
 history will not be available.
Unable to enumerate stack logging records: (os/kern) failure (ox5).
設定は実行可能ファイルの情報 - 引数を開き「環境に設定される変数」へ MallocStackLoggingNoCompact, YES と設定する。
この設定を有効にしてプログラムを実行するとデバッグコンソールに関連するメッセージが起動時に表示されるようになる。
bash(94048) malloc: recording malloc stacks to disk using standard recorder
bash(94048) malloc: stack logging compaction turned off; size of log files on disk
 can increase rapidly
bash(94048) malloc: process 94034 no longer exists, stack logs deleted from
 /tmp/stack-logs.94034.ArrayControllerUndoSample.kYfauH.index
bash(94048) malloc: stack logs being written into /tmp/stack-logs.94048.bash.Bi3fK8.index
arch(94048) malloc: recording malloc stacks to disk using standard recorder
arch(94048) malloc: stack logging compaction turned off; size of log files on disk can
 increase rapidly
arch(94048) malloc: stack logs deleted from /tmp/stack-logs.94048.bash.Bi3fK8.index
arch(94048) malloc: stack logs being written into /tmp/stack-logs.94048.arch.iH8G41.index
実行中...
ArrayControllerUndoSample(94048) malloc: recording malloc stacks to disk using standard recorder
ArrayControllerUndoSample(94048) malloc: stack logging compaction turned off;
 size of log files on disk can increase rapidly
ArrayControllerUndoSample(94048) malloc: stack logs deleted from
 /tmp/stack-logs.94048.arch.iH8G41.index
ArrayControllerUndoSample(94048) malloc: stack logs being written into
 /tmp/stack-logs.94048.ArrayControllerUndoSample.psd1af.index


info stack


スタックのバックトレースを表示する。Xcodeのデバッグツールでも表示できるがコマンド実行できると何かと便利。
(gdb) info stack
#0  -[CustomArrayController _addObserverFor:] (self=0x10012d9a0, _cmd=0x100003487, 
objects=0x100557600) at /Users/hashi/Development/CustomArrayController.m:145
#1  0x0000000100002560 in -[CustomArrayController insertObject:atArrangedObjectIndex:]
 (self=0x10012d9a0, _cmd=0x7fff8500a525, object=0x1005588c0, index=0) at
 /Users/hashi/Development/ArrayControllerUndoSample/CustomArrayController.m:171
#2  0x00007fff84a5b156 in -[NSArrayController addObject:] ()
#3  0x00007fff84b779d3 in -[NSArrayController _executeAdd:didCommitSuccessfully:actionSender:] ()
#4  0x00007fff84ad6329 in _NSSendCommitEditingSelector ()
#5  0x00007fff84ad7c97 in -[NSController _controllerEditor:didCommit:contextInfo:] ()
#6  0x00007fff83e9896c in __invoking___ ()
#7  0x00007fff83e9883d in -[NSInvocation invoke] ()
#8  0x00007fff83eb4711 in -[NSInvocation invokeWithTarget:] ()
#9  0x00007fff88deb23c in __NSFireDelayedPerform ()
#10 0x00007fff83e5fbe8 in __CFRunLoopRun ()
#11 0x00007fff83e5ddbf in CFRunLoopRunSpecific ()
#12 0x00007fff86b5d91a in RunCurrentEventLoopInMode ()
#13 0x00007fff86b5d67d in ReceiveNextEventCommon ()
#14 0x00007fff86b5d5d8 in BlockUntilNextEventMatchingListInMode ()
#15 0x00007fff84913e64 in _DPSNextEvent ()
#16 0x00007fff849137a9 in -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:] ()
#17 0x00007fff848d948b in -[NSApplication run] ()
#18 0x00007fff848d21a8 in NSApplicationMain ()
#19 0x0000000100001abd in main (argc=1, argv=0x7fff5fbff4d0) at
 /Users/hashi/Development/MacOSX-Sample-Code/ArrayControllerUndoSample/main.m:13

[iOS] UIWebView でリッチテキスト形式のファイル(*.rtfd)を表示する

2010年12月29日水曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: [iOS] UIWebView でワード書類を表示する

Mac OS X では意外と使いでのあるリッチテキスト形式のファイルも試してみた。

サンプル


元となるリッチテキスト形式のファイルは図入りのものを用意した(Mac OS X 10.6上のテキストエディットで作成した)。拡張子は *.rtfd になる。

iPhoneシミュレータでの実行結果。

iPadシミュレータでの実行結果。悪くない。

全体的に再現性は高い。もともとリッチテキストの表現能力が高くないせいだろう。


実装


基本的にこれまでと同じ
Cocoaの日々: [iOS] UIWebView でエクセルシートを表示する

図入りのリッチテキスト形式の場合、拡張子が .rtfd で実体はフォルダ(パッケージ)となる。UIWebView で読み込む場合は、この rtfdをZIP圧縮して *.rtfd.zip という形で使う。
- (void)load4:(id)sender
{
 NSURL* url = [[NSBundle mainBundle] URLForResource:@"sample4"
           withExtension:@"rtfd.zip"];
 NSURLRequest* req = [NSURLRequest requestWithURL:url];
 
 [self.webView loadRequest:req]; 
 
}

[参考] Technical Q&A QA1630: Using UIWebView to display select document types
上記より引用:

IMPORTANT: Rich Text Format Directory (.rtfd) documents are document packages and must be ZIP compressed for UIWebView to recognize them. You must retain both extensions in the file name, such as document.rtfd.zip.
iWork '09 documents do not use a package format and must not be ZIP compressed.


ソースコード


GitHub からどうぞ。
xcatsan/iOS-Sample-Code at 2010-12-29 - GitHub



備考


RTFDは開発ドキュメントを書くのに意外と役立つ。Xcodeだと標準で表示・編集ができて Subversion/Git にソースコードと一緒に同梱することができる。実装情報やバージョンアップ時の運用方法などソースコードの運用に必要なドキュメントに使うと良いと思う。

[iOS] UIWebView でワード書類を表示する

2010年12月28日火曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: [iOS] UIWebView でパワーポイントファイルを表示する

エクセル、パワーポイントときたら次はワードでしょ、という訳で試してみた。


サンプル


元のファイル。Word 2004 に付いていたテンプレートを2つ組み合わせて2ページの書類を作った。
※Microsoft Word 2004を使用


iPhoneシミュレータでこんな感じ。レイアウトが崩れている。

iPadシミュレータでこんな感じ。


レイアウトは崩れるが最低限読むことができるといったレベルだ。


実装方法はエクセルの時と同じ。
Cocoaの日々: [iOS] UIWebView でエクセルシートを表示する

ソースコード


GitHub からどうぞ。
DisplayingExcelFile at 2010-12-28 from xcatsan/iOS-Sample-Code - GitHub

[iOS] UIWebView でパワーポイントファイルを表示する

2010年12月27日月曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: [iOS] UIWebView でエクセルシートを表示する [2] 実機確認

今回はパワーポイントのファイル。ウィザードで作成したサンプルを表示させてみる。

※Microsoft PowerPoint 2004を使用

サンプル


元の pptファイル。
iPhoneシミュレータでの表示。スライドが縦につながった形で表示される。

iPad だと表示領域が広がった分見やすい。

表示の互換性は意外とあったという感じ。

実装方法はエクセルの時と同じ。
Cocoaの日々: [iOS] UIWebView でエクセルシートを表示する


ソースコード


GitHub からどうぞ。
DisplayingExcelFile at 2010-12-27 from xcatsan/iOS-Sample-Code - GitHub

[iOS] UIWebView でエクセルシートを表示する [2] 実機確認

2010年12月26日日曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: [iOS] UIWebView でエクセルシートを表示する

前回のコードを iPad対応させて実機で確認してみた。

シミュレータとほとんど同じ表示(当たり前だが..)。フォントは元のファイルで 11pt 指定だったが実機では一回り小さく見える(8か9ptぐらい?)。


[参考] Cocoaの日々: 簡易スライドビューア [7] iPad対応〜ユニバーサルアプリを作る

[iOS] UIWebView でエクセルシートを表示する

2010年12月25日土曜日 | Published in | 0 コメント

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

iPad でエクセルシートを表示する必要が出てきたので試してみた。

方法


UIWebView を使うと表示ができるようだ。方法は iOS Reference Library の QA1630 に記述されている。

Technical Q&A QA1630: Using UIWebView to display select document types

読み込み可能な書類の種類は次の通り(上記サイトより転載)。
Excel (.xls)
Keynote (.key.zip)
Numbers (.numbers.zip)
Pages (.pages.zip)
PDF (.pdf)
Powerpoint (.ppt)
Word (.doc)
Rich Text Format (.rtf)
Rich Text Format Directory (.rtfd.zip)
Keynote '09 (.key)
Numbers '09 (.numbers)
Pages '09 (.pages)


サンプル


エクセルシートのサンプルを用意して表示させてみた。元のファイルはこんな感じ。
※Microsoft Excel 2004を使用

 


iPhone シミュレータで実行するとこんな感じになる。




簡略化された感じになる。端折られた文字情報もある。最低限の確認用といった感じか。


実装


URL/Request を用意して -[UIWebView loadRequest:] で読み込むだけ。
- (void)load:(id)sender
{
 NSURL* url = [[NSBundle mainBundle] URLForResource:@"sample"
           withExtension:@"xls"];
 NSURLRequest* req = [NSURLRequest requestWithURL:url];
 
 [self.webView loadRequest:req];
}

ソースコード


GitHub からどうぞ。
DisplayingExcelFile at 2010-12-25 from xcatsan/iOS-Sample-Code - GitHub

[Mac] NSArrayController に Undo/Redo を実装する [3] 改良

2010年12月24日金曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: [Mac] NSArrayController に Undo/Redo を実装する [2] カスタムモデル

前回のコードを改良した。


改良点


前回はレコードの挿入毎にプロパティの一覧を取得していた。NSArrayController 初期化時にモデルのクラスは Nibからわかるのでこのタイミングで KVO監視対象のリストを作ってしまうようにした。
- (void)_setup
{
 self.undoManager = [[[NSUndoManager alloc] init] autorelease];
 self.keys = [self _propertyListOfClass:[self objectClass]];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
 self = [super initWithCoder:aDecoder];
 if (self != nil) {
  [self _setup];
 }
 return self;
}
これに伴い挿入処理でプロパティリストを取得する処理が不要になった。

またプログラミングで setObjectClass: が呼ばれた場合を想定してこれをオーバーライドしておく。
{
 [super setObjectClass:objectClass];
 self.keys = [self _propertyListOfClass:objectClass];
}


ソースコード


GitHub からどうぞ。
ArrayControllerUndoSample at 2010-12-24b from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSArrayController に Undo/Redo を実装する [2] カスタムモデル

2010年12月23日木曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: [Mac] NSArrayController に Undo/Redo を実装する

モデルのクラスを NSMutableDictionary ではなくカスタムモデル(クラス)を使うケースを考えてみた。


カスタムモデル


こんなクラスを定義してみた。
@interface Book : NSObject {

 NSString* titile_;
 NSString* author_;
}
@property (nonatomic, copy) NSString* title;
@property (nonatomic, copy) NSString* author;

@end
NSMutableDictionary と同等の働きを持つが、値の管理をプロパティで行っている。NSArrayController にモデルのクラスを伝えるには -[NSObjectControlelr setObjectClass:]を使う。
NSObjectController Class Reference

Interface Builder を使えばインスペクタでクラス名を指定できる。


プロパティから監視キーを自動取得する


プロパティはランタイム関数 class_copyPropertyList を使うと特定のクラスで定義されているプロパティ名のリストを取得することができる。これは以前紹介したことがある。

[参照] (旧) Cocoaの日々: @property の一覧を取得する

これを使って KVO登録に必要なキー名の一覧を取得してみた。
- (NSArray*)_propertyListOf:(id)object
{
 NSMutableArray* list = [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];
  const char *propName = property_getName(property);
  NSString *propertyName = [NSString stringWithUTF8String:propName];
  [list addObject:propertyName];
 }
 free(properties);
 return list;
}
これを使い KVO登録を行う。
- (void)_addObserverFor:(NSArray*)objects
{
 for (id object in objects) {
  NSArray* keys = self.keys;
  if (!keys) {
   keys = [self _propertyListOf:object];
  }
  for (NSString* key in keys) {
   [object addObserver:self
      forKeyPath:key
      options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
      context:nil];   
  }
 }
}
結果はOK。前回のようにわざわざ監視キーの一覧を渡さなくて良いのでこちらの方が使い勝手が良い。自分の場合、サンプルを除けば NSMutableDictionary をモデルとして使うことはあまり無くて、どちらかといえば手間をかけてカスタムクラスを定義する方なのでこちらで十分使えそうだ。


備考


前回の初期化コードにはバグがあって Interface Builder で定義した内容が実行時に反映されていなかった。
- (id)initWithCoder:(NSCoder *)aDecoder
{
 // self = [super init];  ※間違い
 self = [super initWithCoder:aDecoder];
 if (self != nil) {
  [self _setup];
 }
 return self;
}
Nib 経由でインスタンス化される場合は initWithCoder: が呼ばれるが、そこで初期化処理を書く場合は親の initWithCoder: を呼ぶ必要がある。それによって Nibで定義した情報を元にインスタンス化されるのだが、前回のように initを読んでしまうと Nibとは無関係に普通に初期化してしまう。


ソースコード


GitHub からどうぞ。 ArrayControllerUndoSample at 2010-12-23 from xcatsan/MacOSX-Sample-Code - GitHub

[Mac] NSArrayController に Undo/Redo を実装する

2010年12月22日水曜日 | Published in | 0 コメント

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

NSArrayController に Undo/Redo を実装してみた。

サンプル


NSTableView → NSArrayController → NSMutableArray[NSMutableDictionary] と繋いだ簡単なサンプルを作った。

NSTableView上での挿入・削除・値変更について Undo/Redo が利用できる。


アーキテクチャ


Undo/Redo の処理を MVC の C、すなわちコントローラで実装した。

Undo/Redoの機能は本来モデルで実装すべき処理かもしれない。Core Data では実際そうしている。ただコントローラで Undo/Redo 機能があると特別なモデルクラス(例えば NSManagedObject)を用意する必要がないというメリットがある。つまり普通のモデルクラスに後からコントローラ側で Undo/Redo機能を追加できる。

今回は NSArrayController のサブクラスを作り、その内部で独自の NSUndoManager を管理するようにした。


実装


今回作成したクラス CustomArrayController のインターフェイス。
@interface CustomArrayController : NSArrayController {

 NSUndoManager* undoManager_;
 BOOL skipFlag_;
 NSArray* keys_;
}
@property (nonatomic, retain, readonly) NSUndoManager* undoManager;
@property (nonatomic, retain) NSArray* keys;

-(void)undo;
-(void)redo;

-(IBAction)undo:(id)sender;
-(IBAction)redo:(id)sender;

@end
NSUndoManager は他のUndoと統合(グループ化)したり外部から操作できるようにプロパティ定義した。keys は値監視対象のキー名を格納する。

実装のポイントはレコードの操作の Undo/Redo処理と、カラム値の Undo/Redo処理。

レコード操作の Undo/Redo処理


これはNSArrayController の下記のメソッドをオーバライドし、そこへ Undo/Redo処理を実装した。
- (void)insertObject:(id)object atArrangedObjectIndex:(NSUInteger)index
- (void)insertObjects:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes
- (void)removeObjectAtArrangedObjectIndex:(NSUInteger)index
- (void)removeObjectsAtArrangedObjectIndexes:(NSIndexSet *)indexes
挿入された時には削除操作をNSUndoManagerへ登録し、反対に削除された時は挿入操作を登録する処理を書く。例えば -insertObject:atArrangedObjectIndex: の実装はこんな感じ。
if (!skipFlag_) {
  [self _addObserverFor:[NSArray arrayWithObject:object]];

  [[self.undoManager prepareWithInvocationTarget:self]  // Undo登録
   removeObjectAtArrangedObjectIndex:index];  
 }
 [super insertObject:object atArrangedObjectIndex:index];
skipFlag_ は2重処理を防ぐために入れてある。複数のオブジェクト挿入を行う insertObjects:atArrangedObjectIndexes: は insertObject:atArrangedObjectIndex: を内部的に呼び出す。今回は insertObjects:atArrangedObjectIndexes: 側にも Undo登録の処理を書いているので、そちらと処理が重ならないようにこのような判定を行っている。-insertObjects:atArrangedObjectIndexes: の実装はこう。
- (void)insertObjects:(NSArray *)objects atArrangedObjectIndexes:(NSIndexSet *)indexes
{
 [self _addObserverFor:objects];

 [[self.undoManager prepareWithInvocationTarget:self]
  removeObjectsAtArrangedObjectIndexes:indexes];

 skipFlag_ = YES;
 [super insertObjects:objects atArrangedObjectIndexes:indexes];
 skipFlag_ = NO;
}

今回の検証でわかったのだが1件だけ削除するケースでは(複数削除)→(単一削除)という処理が走る。しかし複数件削除するケースでは単一削除が呼ばれない。
a) 1件削除の場合
removeObjectsAtArrangedObjectIndexes:
 ↓
removeObjectAtArrangedObjectIndex:

b) 複数件削除の場合
removeObjectsAtArrangedObjectIndexes:
複数削除のメソッドから必ず単一削除のメソッドを呼び出すのなら Undo登録は単一削除側だけで良いのだが、この変則的な呼び出しルールがある為、skipFlag_ を導入した。挿入処理だとルールがまた違ったのだが削除側と対称性を持たせるために同じ処理を書いた。


カラム値の Undo/Redo処理


カラム値の Undo/Redo処理を実装するには、NSArrayController のバインド先のモデルの値の変化イベントとその値が必要となる。今回はレコード挿入時に各カラム値をKVO登録してその変化を取得するようにした。先ほどの挿入コード内で呼んでいた _addObserverFor: がその処理。
- (void)_addObserverFor:(NSArray*)objects
{
 for (id object in objects) {
  NSArray* keys = self.keys;
  for (NSString* key in keys) {
   [object addObserver:self
      forKeyPath:key
      options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
      context:nil];   
  }
 }
}
self.keys はサンプルだと {@"title", @"author"} という配列が入っている。この配列は外部から与えている。本来はモデルからこのキー名を取得できれば良いのだが NSMutableDictionary を NSArrayControllerのモデル(レコード)として使っている場合、レコード新規作成時にはこの情報が取れない。カラムの値(例えば title)を登録した後になって -[NSMutableDictionary allKeys] でキー名が取れるようになる。この為、外部から監視対象のキー名を与えるようになっている。
なお -[NSObject addOberser:forKeyPath:options:context:] に forKeyPath:@"self" を指定した場合は変更イベントを受け取れなかった。

一旦 KVO登録できれば監視対象のカラムの変更イベントをうけとれる。実装はこんな感じ。
- (void)observeValueForKeyPath:(NSString *)keyPath
       ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
 if (change) {
  id value = [change objectForKey:NSKeyValueChangeOldKey];
  if (value == [NSNull null]) {
   value = nil;
  }
  [[self.undoManager prepareWithInvocationTarget:self]
   _setArrangedObject:object value:value forKeyPath:keyPath];   
 }
}
監視対象の値の変化があった時に observeValueForKeyPath:ofObject:change:context: が呼ばれる。変化内容は changeに入っていてこんな感じで渡ってくる。
change: {
    kind = 1;
    new = B;
    old = A;
}
値が nil の場合、NSNull のインスタンスが渡ってくるのでその変換を行っておく。NSUndoManager には _setArrangedObject:value:forKeyPath: を登録しておく。
- (void)_setArrangedObject:(id)object value:(id)value forKeyPath:(NSString*)keyPath
{
 id currentValue = [object valueForKey:keyPath];
 
 [[self.undoManager prepareWithInvocationTarget:self]
  _setArrangedObject:object value:currentValue forKeyPath:keyPath]; 
 
 [object setValue:value forKeyPath:keyPath];
}
このメソッドは現在値を取得して、その値を使って自分自身のメソッドを呼び出すように NSUndoManager へ登録する。こうすると Undo時にもこのメソッドが呼ばれ、その時には Redo用の登録が行われることになる(Undo/Redoの判断は NSUndoManagerが把握していてるので気にする必要はない)。その後モデルのカラム値を setValue:forKeyPath: で設定する。

なおレコードが削除される時には KVOを解除しておく。レコード削除コードでは下記のメソッドを呼び出しておく。
- (void)_removeObserverFor:(NSArray*)objects
{
 for (id object in objects) {
  NSArray* keys = self.keys;
  for (NSString* key in keys) {
   [object removeObserver:self
      forKeyPath:key];
  }
 }
 
}
削除後も NSUndoManager によってレコードのインスタンスは retain されていて、Undo時にまた同じインスタンスがモデル配列へ追加される。この時には NSUndoManagerから通常の操作と同じく insertObject:atArrangedObjectIndex: が呼ばれるため、そこでまた KVO登録処理が走る。この為、Undo/Redoを繰り返すと同じ KVO登録が複数行われ、試した時にはアプリの動作が止まったように遅くなってしまった。これを防ぐために(またリソースを無駄にしない為にも)KVO解除を忘れずにやっておく。


[参考]Cocoaの日々: [Mac] NSArrayController の KVO通知の中身が nil になる(現象記録)


課題


カラムのキー名を外部から与えるというのがいかにもカッコ悪いし使いづらい。Interface Builder で NSArrayControllerのインスペクタを見るとキー名の表示があるのでこれが取得できれば良いのだが方法がわからなかった。ここに表示されるということは Nib内にあるハズなのだが。



ソースコード


GitHub からどうぞ。

ArrayControllerUndoSample at 2010-12-22 from xcatsan/MacOSX-Sample-Code - GitHub


参考情報


(旧) Cocoaの日々: rubberBand(その19)Undoの実装 / NSUndoManager
昔書いた NSUndoManager の説明。

NSUndoManager Class Reference

Key-Value Observing Programming Guide: Introduction to Key-Value Observing Programming Guide

http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CocoaBindings/Concepts/HowDoBindingsWork.html%23//apple_ref/doc/uid/20002373-1…

NSArrayController Class Reference

[お知らせ] サンプルソースコードのライセンスについて

2010年12月21日火曜日 | Published in | 0 コメント

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

当ブログに掲載しているソースコードのライセンスはこれまで明記してきませんでした。もともと無許可で自由に使ってもらって良いと考えていたのですが、たまに利用についての問い合わせがくるのでライセンスを明記した方が良いと思い始めました。

ライセンスの候補としては MIT ライセンスを考えています。
Open Source Initiative OSI - The MIT License:Licensing | Open Source Initiative

MIT License - Wikipedia


他のサイトから借用したコードもあるのでその扱いは個別に考えますが、基本的にこのブログで公開しているソースのライセンスは MITライセンスにしたいと思います。

後日また正式にブログで告知します。またその時以降はソースコードのコメントにもライセンス表記を付けるようにしようと思います。


何か意見やアドバイスがあれば是非教えて下さい。

では。

[Mac] NSArrayController の KVO通知の中身が nil になる(現象記録)

2010年12月20日月曜日 | Published in | 2 コメント

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

NSTableView→NSArrayController→NSMutableArray とバインドしている状態でテーブルのカラムの値の変化を NSArrayControllerのKVO経由で取得してみた。

KVOの登録はこんな感じ。
[_userArrayController addObserver:self
         forKeyPath:@"arrangedObjects.title"
         options:NSKeyValueObservingOptionOld|
         NSKeyValueObservingOptionNew|
         NSKeyValueObservingOptionInitial|
         NSKeyValueObservingOptionPrior
         context:nil];
カラムの編集前後で下記が呼び出される。
- (void)observeValueForKeyPath:(NSString *)keyPath
       ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
        :
}
change を見ると変更前後の値が共に nil になっていて値が取得できなかった。
[81129:a0f] change={
    kind = 1;
    new = <null>;
    old = <null>;
}

既知の問題なのか仕様なのかわからない。ネットで探すと同じ現象が報告されていた。
cocoa - Observer properties of objects in an NSArrayController - Stack Overflow
forKeyPath:@"arrangedObjects.name" 
forKeyPath:@"contentArray.name" 
forKeyPath:@"content.name" 
forKeyPath:@"selection.name" 
forKeyPath:@"selectedObjects.name"
いろんな keyPathを試しているようだが駄目っぽい。

KVO for NSArrayController subclass
ずいぶん昔からあるようなので仕様なのか?

- - - -
NSArrayController を対象に Undo機能を実装しようとしているが KVOが使えないので困っている。仕方がないので NSArrayController のサブクラスを作り対応しようとしているが今度は objectDidBeginEditing: の引数(実体が非公開のNSTextValueBinder)の扱いで困っている。編集前後のイベントは取れるが値が取得できない。うーむ。

[iOS] UIAlertView 上に UIProgressView を載せる [2] キャンセルボタン表示

2010年12月19日日曜日 | Published in | 2 コメント

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

[前回] Cocoaの日々: UIAlertView 上に UIProgressView を載せる

前回のコメントで 335gさんから UIAlertView のサイズ変更とボタン位置の変更ができることを教えてもらったので試してみた。


サンプル


前回のコードでキャンセルボタンをそのまま表示するとこんな風になっていた。

今回の修正を加えるとこうなる。


実装


方法は UIAlertViewDelegate のメソッドを使う。
- (void)willPresentAlertView:(UIAlertView *)alertView
{
 // (1) サイズと位置を変更する
 CGRect frame = alertView.frame;
 frame.origin.y -= 15;
 frame.size.height += 30;
 alertView.frame = frame;
 
 // ボタン位置を変更する
 for (UIView* view in alertView.subviews) {
  frame = view.frame;
  if (frame.origin.y > 80) {
   frame.origin.y += 25;
   view.frame = frame;
  }
 }
}
UAlertViewのデリゲートを設定しておくと表示前に -willPresentAlertView: が呼ばれる。この引数に UIAlertView が渡ってくるので frameを変更すればサイズと位置の変更ができる。またサブビュー内にキャンセルボタンがあるのでこの表示位置を下へ移動させれば重ならなくなる。今回は UIProgressView の表示位置より下のものをすべて 25ピクセル下へずらすようにした。ここはボタンのクラスを狙い撃ちすることもできる。UIAlertView.subviews を見るとこんな構成になっていた。
[74442:207] UIImageView
[74442:207] UILabel
[74442:207] UILabel
[74442:207] UIThreePartButton
[74442:207] UIProgressView
UIThreePartButton がキャンセルボタンなのでこんな感じで判定できる。
if ([view isKindOfClass:NSClassFromString(@"UIThreePartButton")]) {
UIThreePartButton はプライベートなクラスなようなので名前からクラスのインスタンスを取得して比較に使っている。isKindOfClass:[UIButton class] では駄目だったので UIButton のサブクラスではないようだ。


ソースコード


GitHub からどうぞ。
ProgressBarOnAlertView at 2010-12-19 from xcatsan/iOS-Sample-Code - GitHub

[iOS] UIAlertView 上に UIProgressView を載せる

2010年12月18日土曜日 | Published in | 3 コメント

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

元ネタはここから。
NSCoriolisBlog » Blog Archive » Add an UIProgressView or UIActivityIndicatorView to your UIAlertView

サンプル


こんな感じ。
"START"ボタンを押すと UIAlertView がポップアップし、プログレスバーが進んでいく。

プログレスバーが 100% になると UIAlertView を閉じる。


実装


UIAlertView のサブビューとして UIProgressView を追加する。追加位置はプログラム内で決め打ちしている。
- (IBAction)start:(id)sender
{
 UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Information"
               message: @"Please wait..."
                 delegate: self
              cancelButtonTitle: nil
              otherButtonTitles: nil];
 
 UIProgressView* progressView = [[UIProgressView alloc]
   initWithFrame:CGRectMake(30.0f, 80.0f, 225.0f, 90.0f)];
 [alertView addSubview:progressView];
 [progressView setProgressViewStyle: UIProgressViewStyleBar];
 [progressView release];

 [alertView show];
 [alertView release];
 
 progressValue = 0.0f;
 
 NSDictionary* userInfo =
 [NSDictionary dictionaryWithObjectsAndKeys:
  progressView , kProgressViewKey,
  alertView  , kAlertViewKey,
  nil];

 [NSTimer scheduledTimerWithTimeInterval:0.1f
          target:self
           selector:@selector(updateInformation:)
           userInfo:userInfo
         repeats:YES];
}
サンプルでは NSTimer を使って 0.1秒毎にプログレスバーを 1% 進めている。

なおキャンセルボタンを表示するとこんな感じ。
もともと余白が無いところなので仕方が無い。ボタンを出す場合はメッセージの表示をやめてその位置にプログレスバーを表示するしかない。


ソースコード


ProgressBarOnAlertView at 2010-12-18 from xcatsan/iOS-Sample-Code - GitHub

NSCalendar - +currentCalendar より -initWithCalendarIdentifier: を使う

2010年12月17日金曜日 | Published in | 0 コメント

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

覚書。

+[NSCalendar currentCalendar] で取得する NSCalendar のインスタンスは「言語環境 - カレンダー」設定によって決められる。
「和暦」を選んだ場合、-components:fromDate: で取得できる年の値は(当たり前だが)平成の年数となる(例:22)。

+currentCalendar から取得できる NSCalendarのインスタンスは利用環境に依存する挙動となるため、年数計算等で NSCalendar を使用する場合は +currentCalendar を使わず統一したカレンダを利用する。
(例) NSCalendar *calendar = [[NSCalendar alloc]
        initWithCalendarIdentifier:NSGregorianCalendar];
通常は NSGregorianCalendar で良いと思う。


NSCalendar Class Reference

Interface Builder でボタンをカット&ペーストすると Object ID が変わる

2010年12月16日木曜日 | Published in | 0 コメント

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

覚書。

Interface Builder でボタンをカット&ペーストすると Object ID が変わる


/* Class = "NSButtonCell"; title = "Window Shadow"; ObjectID = "518"; */
"518.title" = "Window Shadow";
 ↓
 ↓
/* Class = "NSButtonCell"; title = "Window Shadow"; ObjectID = "1663"; */
"1663.title" = "Window Shadow";


Interface Builder では Identity Inspector で確認ができる。
この値は変更できない。

- - - -

ローカライズ済みのオブジェクトを別のウィンドウやタブへ移動する場合は要注意。ibtool で他のローカライズリソースを生成する時に Object ID が使われるが、カット&ペーストで Object ID が変わると対応メッセージが存在しない場合が出てくる。


Xcode [Tips] 「よく使う項目バー」

2010年12月15日水曜日 | Published in | 0 コメント

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

たまたま見つけた。

メインウィンドウの上部に1行分空きが表示されるようになる。ここへファイルをドラッグ&ドロップすると Safariのようにブックマークできる。


表示制御と表示内容はプロジェクト毎に設定できる。

うまく使えば便利かもしれない。

[iOS] CoreData - マイグレーション[5] マイグレーション中にアプリを終了させたらどうなる?

2010年12月14日火曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: [iOS][Mac] CoreData - マイグレーション[4] モデルファイルの構成

マイグレーションの検証中の素朴な疑問:Core Data のマイグレーション中にアプリを落としたり、電源を落としたりするとどうなるのか?
試してみた。


サンプルプログラム


前回のソースに手を入れて初回起動時に 10,000件のデータを登録するようにしてみた。実行するとデバッグコンソールに 20件毎のコミット状況が書きだされる。
最初にバージョン1のモデルで実行し、次にバージョン2のモデルへ切り替えてマイグレーションが実行されるようにする。

まず最初に正常動作の確認をしたところ次の結果となった。
[バージョン1] サンプルデータ 10,000件の生成: 32秒
[バージョン2] マイグレーション 10,000件  : 24秒
※ iPhone 3GS / iOS 4.2

マイグレーションの開始と終了は NSEntityMigrationPolicy のメソッドを利用してわかるようにした。こんな感じ。
- (BOOL)beginEntityMapping:(NSEntityMapping *)mapping
 manager:(NSMigrationManager *)manager error:(NSError **)error
{
 NSLog(@"Begin migration");
 return YES;
}
- (BOOL)endEntityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager
 error:(NSError **)error
{
 NSLog(@"End migration");
 return YES;
}



アプリ終了


次にマイグレーション実行中にアプリケーションを停止させてみた。ログを見ていると終了後も処理は実行されていてマイグレーションが完了した。どうも自動的にバックグラウンドでマイグレーション処理が継続して実行されているようだ。ちなみにサンプルプログラムはバックグラウンド動作の指定はしていない。UIApplicationExitsOnSuspend は True になっている。


アプリ強制終了


Fast App Switching でアプリを強制終了させても動作し続けていた。うーむ。
※この後は検証していない


電源オフ


マイグレーション実行中にアプリを終了させ、さらに電源をオフにしてみた。しかしこの間もバックグラウンドで動き続けていた。10,000件のデータの場合、処理時間が長いのでバックグラウンドの10秒ルールに抵触して強制終了させられてしまった。
SpringBoard[27] <Warning>: CoreDataMigSamp[1241] has active assertions beyond permitted time:
     {(
         <SBProcessAssertion: 0x5c53d80> identifier: Suspending process: CoreDataMigSamp[1241] 
           permittedBackgroundDuration: 10.000000 reason: suspend owner pid:27 preventSuspend
           preventThrottleDownCPU  preventThrottleDownUI
     )}
SpringBoard[27] <Warning>: Forcing crash report of CoreDataMigSamp[1241]...
SpringBoard[27] <Warning>: Finished crash reporting.
ReportCrash[1258] <Error>: Saved crashreport to /var/mobile/Library/Logs/Cra...
Tcom.apple.launchd[1] <Notice>: (UIKitApplication:com.yourcompany.
  CoreDataMigSample[0x11dc]) Exited: Killed
処理件数 3,000件を試した時にはマイグレーション処理が10秒以内に収まったので、この時は電源オフ後にマイグレーションが完了した。

さて途中で Killされた後、アプリを起動するとどうなるのか。
CoreDataMigSample[1389:307] Removed orphaned, partially migrated store file
 file://localhost/var/mobile/Applications/A7DFB71B-5B24-41D4-A871-
 F2AA18369684/Documents/.CoreDataMigSample.sqlite.migrationdestination_
 41b5a6b5c6e848c462a8480cd24caef3
CoreDataMigSample[1389:307] Begin migration
CoreDataMigSample[1389:307] End migration
前回の失敗が破棄されてマイグレーションが最初から実行され、そして完了した。おお、Core Data すごいぜ。。


考察


Core Data のマイグレーションは1箇所の変更であっても、新DB作成→旧DBからのマイグレーションという手続きを取る(新DBはマイグレーション成功後に正式な名前に替えられて旧DBと置き換わる。旧DBは名前が変更されてバックアップとして残る)。その為に件数が多い場合は時間がかかる。このマイグレーションに時間がかかる場合 iOS デバイスだと途中でアプリを終了させられたり電源を落とされる可能性が高い。それ故、Core Dataを使ったアプリのバージョンアップに関する大きな懸念点の一つとしてずーっと気になっていた。これが今回の検証で不整合が基本的に発生しない作りになっていることが確認できたのは大きい。マイグレーションのバックグラウンド動作は iOS 4.0 からなのかは確認できていないが、少なくとも iOS 4.0以上をターゲットにしたアプリケーションで Core Data を使う場合の懸念材料が一つ減った。機能面と合わせ、運用面での性能も充実してきたので iOSデバイスでも十分に実用に耐えるのではないかと思う。


ソースコード


GitHub からどうぞ。
CoreDataMigSample at 2010-12-14 from xcatsan/iOS-Sample-Code - GitHub


- - - -
後はマイグレーション処理中の経過をユーザへフィードバックできればいいな。後ほど調べてみよう。

ネットワーク接続状況を知る[2] SCNetworkReachabilityGetFlags はブロックする

2010年12月13日月曜日 | Published in | 0 コメント

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

[2011-07-08追記] ブロックの原因が判明、下記もどうぞ。
Cocoaの日々: [iOS] SCNetworkReachabilityGetFlags のブロックの件


以前、Cocoaの日々: ネットワーク接続状況を知る というブログを書いた。

その後わかったことを紹介する。

SCNetworkReachabilityGetFlags がブロックする件


ネットワーク接続状況を取得する SCNetworkReachabilityGetFlags関数 が処理中にブロックしてしまうことがわかった。次の環境でブロックする現象が確認できた。

・iPhone 3GS/iOS4.2
・3GS→× WiFi→◯ ※フライトモード+WiFi有効
・WiFiルータ→×   ※インターネット非接続(光回線断)

つまり iPhone で WiFi接続しているが、その接続先の WiFiルータから先のインターネットに接続していない。この環境で関数を呼ぶとそこで処理が停止し、60秒後にアプリケーションの動作が再開する。60秒を待たずにアプリケーションが落ちたケースもあった(クラッシュログから SCNetworkReachabilityGetFlags が原因であることが分かっている)。

タイムアウトは実測値で60秒。変更できそうな関数は見当たらない。

このことからSCNetworkReachability系の API を利用する場合は、SCNetworkReachabilityGetFlags は使わず、コールバック関数を使った非同期実装が良いと思われる。


サンプルプログラム NetworkRechable の改良


今回の情報を元に以前作成したサンプルプログラムを改良した。SCNetworkReachabilityGetFlags の使用をやめて SCNetworkReachabilitySetCallback を使った非同期方式に全面的に書き換えた。

インターフェイスはこう。
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>

typedef enum {
 kNetworkReachableUninitialization = 0,
 kNetworkReachableNon,
 kNetworkReachableWiFi,
 kNetworkReachableWWAN
} NetworkReachabilityConnectionMode;

#define NetworkReachabilityChangedNotification @"NetworkReachabilityChangedNotification"

@interface NetworkReachability : NSObject {

 SCNetworkReachabilityRef reachability_;
 NetworkReachabilityConnectionMode connectionMode_;
}

+ (NetworkReachability*)networkReachabilityWithHostname:(NSString *)hostname;
- (NetworkReachabilityConnectionMode)connectionMode;
- (NSString*)connectionModeString;

@end
実装はポイントだけ。
// 初期化
- (id)initWithHostname:(NSString*)hostname
{
 if (self = [super init]) {
  reachability_=
  SCNetworkReachabilityCreateWithName(kCFAllocatorDefault,
           [hostname UTF8String]);
  connectionMode_ = kNetworkReachableUninitialization;  
  [self startNotifier_];
 }
 return self;
}

// コールバック設定
- (BOOL)startNotifier_
{
 BOOL ret = NO;
 SCNetworkReachabilityContext context = {0, self, NULL, NULL, NULL};
 if(SCNetworkReachabilitySetCallback(reachability_, ReachabilityCallback_, &context))
 {
  if(SCNetworkReachabilityScheduleWithRunLoop(
             reachability_, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode))
  {
   ret = YES;
  }
 }
 return ret;
} 

// コールバック先
static void ReachabilityCallback_(SCNetworkReachabilityRef target,
 SCNetworkReachabilityFlags flags, void* info)
{
 NSAutoreleasePool* myPool = [[NSAutoreleasePool alloc] init];
 
 NetworkReachability* noteObject = (NetworkReachability*)info;
 [noteObject updateConnectionModeWithFlags_:flags];
 
 [[NSNotificationCenter defaultCenter]
  postNotificationName:NetworkReachabilityChangedNotification object:noteObject];
 
 [myPool release];
}
内部的に SCNetworkReachabilitySetCallback を使い、コールバックされたタイミングで connectionMode_ を更新する。コールバックは接続状況に変化があったときに呼び出される(例えば、3G→WiFiや、WiFi→OFF)。


利用イメージ


NetworkReachability* networkReachability =
  [NetworkReachability networkReachabilityWithHostname:@"www.google.com"];
NetworkReachabilityConnectionMode mode = [networkReachability connectionMode];
何度も使う場合はインスタンスをとっておいて使い回せば良い。

接続状況が変化した時に通知を受け取りたい場合は NetworkReachabilityChangedNotification を監視すれば良い。
[[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(reachabilityChanged:)
             name:NetworkReachabilityChangedNotification
              object: nil];
- (void) reachabilityChanged: (NSNotification* )note
{
 NSLog(@"changed:%@", note);
 [self updateStatus];
}


備考


初回のタイムラグ

コールバックを使った非同期式の場合、接続状況が変化した時の通知に若干のタイムラグがある(長くはない〜1秒未満)。多くの場合問題ないがアプリケーション起動時の最初の処理でこの接続状況を使う場合は問題になることがある。実際簡単なアプリケーションでは最初の画面が表示された後に最初の変更通知が届くので、最初の画面でネットワークの接続状況を確認してからアクションを起こすという処理の場合このタイムラグが問題になる。非同期処理だから本質的な解決方法は無いので、NetworkReachabilityChangedNotification の到着を待ってから処理を行うか、初回のみ接続状況は無視するような対処が必要になる。先のサンプルではこの接続状況が決まらない状態(すなわち最初の通知が来るでの状態)として kNetworkReachableUninitialization という初期値を設定している。利用側は -[NetworkReachability connectionMode] でこの値が帰ってきた場合、このタイムラグ中であることが判断できるので、それを元になんらかの対処を行う方法が考えられる。

SCNetworkReachabilityCreateWithName()で指定するホスト

SCNetworkReachability インスタンスを作成する時に使う SCNetworkReachabilityCreateWithName 関数へ渡すホスト名は現実に存在してネットワークで到達可能なものを指定する必要がある。試しに dummy.dummy.dummy のような適当なホスト名を渡したところ、常に接続不能状態となってしまった(当たり前といえば当たり前だが...)。



ソースコード


GitHubからどうぞ。
NetworkReachable at 2010-12-13 from xcatsan/iOS-Sample-Code - GitHub



参考情報


SCNetworkReachability Reference

[iOS][Mac] CoreData - マイグレーション[4] モデルファイルの構成

2010年12月12日日曜日 | Published in | 0 コメント

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

[前回] Cocoaの日々: [iOS][Mac] CoreData - マイグレーション[3] エラー

モデルファイルの構成・配置についてのまとめ。


モデルファイルの構成〜Xcode


XcodeでCore Data のモデルを定義すると <modelname>.xcdatamodeld というグループが作られ、その下に <modelname>.xcdatamodel ファイルが配置される。モデルファイルをバージョンアップするとこのグループ内にバージョン毎のモデルファイルが追加されていく。下記はバージョン1とバージョン2が存在する例。


モデルファイルの構成〜実体


<modelname>.xcdatamodeld の実体はパッケージ(フォルダ)である。
Finderで見るとこう。
パッケージの中身を見ると <modelname-version>.xcdatamodel が格納されているのがわかる。


モデルファイルの構成〜実行時


これをビルドするとその結果アプリケーションバンドル内には <modelname>.momd というフォルダが作成され、その中に <modelname-version>.mom ファイルが格納される。先ほど例に上げたファイルの場合、HairConcierge.momd というパッケージ内に HairConcierge.mom, HairConcierge 2.mom が格納される。
VersionInfo.plist はバージョン管理情報が格納されている。中身はこんな感じ。
どのバージョンのモデルを使っているのか、またそれぞれのバージョンで使用しているエンティティのリストが格納されている。

なお、複数のバージョンが存在しない場合(一番最初のバージョンなど)は <modelname>.momd は存在せず、バンドルフォルダのルートレベルに <modelname>.mom ファイルが設置される。
(単一、複数で配置場所が変わることがバージョンアップ時の問題を引き起こす。問題の対応方法については前回の記事を参照のこと)


トラブル


先日訳あってモデルファイル名を変更した。具体的には頭大文字を小文字にした。
HairConcierge.xcdatamodeld → hairConcierge.xcdatamodeld
それと同時にモデルファイルをバージョンアップし、マイグレーションの設定を行った。

この新しいバージョンのアプリをビルドして古いバージョンを上書きインストールしたところマイグレーションが行われない現象が出た。原因がわからずいろいろ調べているとアプリケーションバンドル内のモデルファイルが旧バージョンしか存在しないことがわかった。
HairConcierge.momd
         |--HairConcierge.mom
         |--VersionInfo.plist
本来は新しいバージョンのモデルファイル HairConcierge 2.mom が存在するはず。??

さらに調べると momdフォルダが2つ存在することが判明。
HairConcierge.momd
         |--HairConcierge.mom
         |--VersionInfo.plist

hairConcierge.momd
         |--hairConcierge.mom
         |--hairConcierge 2.mom
         |--VersionInfo.plist
原因はなんとモデルファイルの名前を変えたことだった。頭を大文字から小文字に変えたため、旧バージョンのモデルファイルと別扱いになっていた。マイグレーションが実行されなかったのは、2つの momdフォルダのうち旧バージョンだけが含まれる前者を使っていたから。なんてこった。

旧バージョンは既に App Store で配布しているので今回は名前を元に戻すしか無い(大文字に戻す)。実際、名前を元に戻したところ、上記のような2つの momd フォルダは作成されず、無事にマイグレーションが行われた。構成はこんな感じになる。
HairConcierge.momd
         |--HairConcierge.mom
         |--HairConcierge 2.mom
         |--VersionInfo.plist


教訓:リリース後にモデルファイル名を変えてはいけない!

[参考] Cocoaの日々: [iOS] プロダクト名を変えてはいけない

[iOS] GameKit - WiFi接続

2010年12月11日土曜日 | Published in | 2 コメント

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

[前回] Cocoaの日々: [iOS] GameKit - Bluetoothを使った iOSデバイス間の通信 [2]画像の送受信

Bluetooth に代えてWiFiを使ったピア接続をやってみる。

GKPeerPickerConnectionTypeOnline


WiFi経由で接続する場合は GKPeerPickerController.connectionTypesMask に GKPeerPickerConnectionTypeOnline を加える。
picker.connectionTypesMask = GKPeerPickerConnectionTypeOnline|GKPeerPickerConnectionTypeNearby;
GKPeerPickerConnectionTypeOnline だけでは駄目で GKPeerPickerConnectionTypeNearby を一緒に指定する必要がある。設定しない場合エラーとなる。
[2152:307] *** Terminating app due to uncaught exception 'GKInvalidArgumentException',
 reason: 'A GKPeerPickerController must support GKPeerPickerConnectionTypeNearby at a minimum.'

すると最初に WiFiと Bluetoothのどちらを使うか選択ダイアログが表示されるようになる。


GKSession作成


WiFi の場合は GKPeerPickerControllerDelegate の -[GKPeerPickerController peerPickerController:didSelectConnectionType:] を実装する。
- (void)peerPickerController:(GKPeerPickerController *)picker
 didSelectConnectionType:(GKPeerPickerConnectionType)type
{
 if (type == GKPeerPickerConnectionTypeOnline) {
  picker.delegate = nil;
  [picker dismiss];
  [picker autorelease];
  
  self.session = [[[GKSession alloc] initWithSessionID:nil
             displayName:nil
             sessionMode:GKSessionModePeer] autorelease];
  self.session.delegate = self;
  self.session.available = YES;
  [self.session setDataReceiveHandler:self withContext:nil];
 }
}
WiFi の場合 GKPeerPickerController がやってくれるのはここまでで、これ以降は接続まで自前で実装する必要がある(Bluetoothの場合は接続可能な端末がリストアップされ選択=接続することができる)。

GKSession を作成した後、-[GKSessionDelegate session:peer:didChangeState:] が呼び出されて GKSessionが利用可能になる(GKPeerStateAvailable)。その時点で接続可能な端末が見つかると割り当てられた peerID から端末名を取得することができる。これをリストアップすれば Bluetoothの場合と同様に接続可能リストが表示できそうだ。今回は無条件に connectToPeer:withTimeout: を呼び出して見つかった端末へ接続しに行く。
- (void)session:(GKSession *)session peer:(NSString *)peerID
 didChangeState:(GKPeerConnectionState)state
{
 switch (state) {
  case GKPeerStateAvailable:
   NSLog(@"connecting to %@ ...", [session displayNameForPeer:peerID]);
   [session connectToPeer:peerID withTimeout:10];
   break;
   
  case GKPeerStateConnected:
   self.message.text = @"connected";
   self.peerID = peerID;
   break;
  case GKPeerStateDisconnected:
   self.message.text = @"disconnected";
   self.session = nil;   
  default:
   break;
 }
}
このプログラムを2台の端末で実行すると両方で接続要求をだすので片方はエラーが出る。
Error Domain=com.apple.gamekit.GKSessionErrorDomain Code=30510
 "Connection to peer already in progress after initially succeeding." UserInfo=0x9342b10 
{NSLocalizedFailureReason=Found in progress after success., NSLocalizedDescription=Connection
 to peer already in progress after initially succeeding.}
しかし、その後片方が成功して接続が確立する。後は Bluetooth同様にデータの送受信が可能。


ソースコード


BluetoothSample at 2010-12-11 from xcatsan/iOS-Sample-Code - GitHub


参考情報


Game Kit Programming Guide: About Game Kit

iOS Reference Library
Game Kit Programming Guide の日本語版(PDF)が提供されている。

[iOS] GameKit - Bluetoothを使った iOSデバイス間の通信 [2]画像の送受信

2010年12月10日金曜日 | Published in | 2 コメント

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

[前回] Cocoaの日々: [iOS] GameKit - Bluetoothを使った iOSデバイス間の通信

今回は画像を送信してみた。

サンプル


画像が選ぶと...

こうなる。転送は2秒程度。


解説


まず送信部分。UIImagePickerController の扱いは割愛する。UIImage を送る部分だけピックアップした。
- (void)imagePickerController:(UIImagePickerController *)picker
 didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    [picker dismissModalViewControllerAnimated:YES];
 
 UIImage* image = [info objectForKey:UIImagePickerControllerEditedImage];
 
 NSError* error = nil;
 NSData* data = UIImageJPEGRepresentation(image, 0.5);
 [self.session sendData:data
       toPeers:[NSArray arrayWithObject:self.peerID]
     withDataMode:GKSendDataReliable
      error:&error];
 if (error) {
  NSLog(@"%@", error);
 }
 
}
特別な工夫はなく UIImage を NSData に変換して送信しているだけ。

次に受信部分。
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
 inSession:(GKSession *)session context:(void *)context
{
  // image
  NSLog(@"received image");
  self.imageView.image = [UIImage imageWithData:data];
 }
}
これも工夫は無い。送られてきた NSData から UIImageを生成して表示しているだけ。

なお大きなサイズだとエラーが出ていた。
Error Domain=com.apple.gamekit.GKSessionErrorDomain Code=30202 "Send data error."
 UserInfo=0x570c1b0 {NSLocalizedFailureReason=AGPSessionSendTo failed (801C0001).,
 NSLocalizedDescription=Send data error.}
メモリが不足していたのでそれに関連してエラーが出たのかもしれない。


ソースコード


GitHub からどうぞ。
xcatsan/iOS-Sample-Code at 2010-12-10b - GitHub

[iOS] GameKit - Bluetoothを使った iOSデバイス間の通信

2010年12月9日木曜日 | Published in | 0 コメント

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

GameKit を使い端末間の Bluetooth通信を試してみた。

サンプル


テキストメッセージをやりとりできる簡易なサンプルプログラムを組んでみた。
2つの iOS端末でプログラムを立ち上げて両方で "Connect" を押す。すると近くの端末を探し始める。

Bluetooth がオフに鳴っている場合はオンにするか尋ねられる。
通信可能な端末が見つかるとリストアップされる。
選択すると相手側で受け入れの確認がある。
受け入れる(Accept)と接続が成立する。
 テキストを入れて "send" ボタンを押すと相手側の端末にそのテキストが表示される。


解説


Bluetooth を使った端末間通信を行うのに今回は GameKit.framework を使った。最初にこのフレームワークをプロジェクトへ加えておく。

実装のポイントは2つ。
1. GKPeerPickerController を使い接続を確立する
2. 接続後は GKSession を使いデータの送受信を行う

まず 1. の接続確立から。
- (IBAction)connect:(id)sender
{
 GKPeerPickerController* picker = [[GKPeerPickerController alloc] init];
 picker.delegate = self;
 picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
 [picker show];
}
インスタンスを作り、-[show] を投げると接続に必要なダイアログが表示される。
この後は接続の確立もしくはキャンセルのタイミングで GKPeerPickerControllerDelegate のメソッドが呼ばれる。
接続が確立すると -[peerPickerController:didConnectPeer:] が呼ばれる。接続してしまえば GKPeerPickerController は不要になるので閉じておく。接続後に渡される GKSession を取っておく。この GKSession をこの後のデータ送受信で使う。
- (void)peerPickerController:(GKPeerPickerController *)picker
 didConnectPeer:(NSString *)peerID toSession:(GKSession *)session
{
 self.peerID = peerID;
 self.session = session;
 session.delegate = self;
 [session setDataReceiveHandler:self withContext:nil];
 picker.delegate = nil;
 [picker dismiss];
 [picker autorelease];
}

続いて 2. のデータ送受信を行う。まず送る側から。UITextField 内の文字列を UTF8エンコードして NSDateに変換し、これを送信する。
- (IBAction)sendText:(id)sender
{
 NSData* data = [self.sendText.text dataUsingEncoding:NSUTF8StringEncoding];

 NSError* error = nil;
 [self.session sendData:data
       toPeers:[NSArray arrayWithObject:self.peerID]
     withDataMode:GKSendDataReliable
      error:&error];
 if (error) {
  NSLog(@"%@", error);
 }
 self.sendText.text = @"";
}
次に受け側。receiveData:fromPeer:inSession:context: を実装する。ここでは渡ってきた NSData を UTF8でデコードして NSStringに戻し、画面上の UITextView へ表示している。
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
 inSession: (GKSession *)session context:(void *)context
 NSString* msg = [[[NSString alloc] initWithData:data
   encoding:NSUTF8StringEncoding] autorelease];
 NSString* text = [self.textView.text stringByAppendingFormat:@"%@\n", msg];
 self.textView.text = text;
}

最低限のコードはこれだけ。実に簡単に Bluetooth 通信が実現できる。


ソースコード


GitHub からどうぞ。
BluetoothSample at 2010-12-09 from xcatsan/iOS-Sample-Code - GitHub


備考


Mac Book Pro 上で動かしている iPhoneシミュレータでは Bluetooth の接続確認は行えなかった。Bluetooth自体は有効になるのだが iPhoneを見つけてくれない。シミュレータでテストできるとかなり楽なのだが。今は iPhone 3GS, iPad 両方にいちいちインストールしてテストしている。WiFi接続に切り替えられるようにするとシミュレータでも動くかもしれないので後で試してみよう。

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