非同期保存などのMacOSX10.7の新機能が気になったので、サンプルコードを作って見ました。
まず、一番簡単なのはフルスクリーンモード。単純にNSWindowのプロパティでFull ScreenをPrimary Windowに設定するだけで、ウインドウの右上にフルスクリーンボタンが付きます。
つぎに、Auto Layout。当初、XcodeではGUIでの編集がサポートされていないと思っていましたが、実はサポートされていました。しかし、分かりづらい。
.xibファイルのプロパティで、「Use Auto Layout」にチェックを入れると、レイアウトの編集が通常モードからAuto Layoutモードへと切り替わります。
Editorメニュー配下にPinメニューがが追加されて、これでNSLayoutConstraintオブジェクトが追加できるようになります。
Auto LayoutではPriorityという、そのレイアウトをどの程度強制するのかを示すプロパティの使い方がポイントのようです。例えば、自由にリサイズするビュー内部をレイアウトするとき、すべてをPriority=1000にしてしまうと、ビューのサイズが縮小し過ぎたときにレイアウトを維持できなくなる可能性があるため、実行時に警告が出たりします。
そして、自動保存とバージョン管理については、プロジェクトのテンプレートにあるように +autosavesInPlaceでYESを返すようにするだけで有効になります。
@implementation MyDocument
+ (BOOL)autosavesInPlace
{
return YES;
}
- (IBAction)editModelObject:(id)sender
{
[modelObject edit];
[self updateChangeCount:NSChangeDone];
}
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError
{
return [NSArchiver archivedDataWithRootObject:modelObject];
}
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName
error:(NSError **)outError
{
[self willChangeValueForKey:@"modelObject"];
modelObject = [NSUnarchiver unarchiveObjectWithData:data];
[self didChangeValueForKey:@"modelObject"];
return YES;
}
updateChangeCount:の呼び出しにより書類の編集が検知されると、一定時間操作が無いときにRunLoopから書類の保存が起動されます。また、「保存」メニューが「バージョンを保存」に変わります。readFromData:ofType:error:は、通常書類を開く時に呼ばれますが、書類を古いバージョンに戻す時にも呼ばれます。古いバージョンに戻すときは、開くときと違い、NSDocumentオブジェクトが新規に生成されないので、ここでdidChangeValueForKey:などを呼んでおかないと、表示が更新されません。
で、ここからが本題の非同期保存ですが、まず非同期保存を有効にするには上記のコードに加えて、canAsynchronouslyWriteToURL:ofType:forSaveOperation:でYESを返し、保存処理の中でunblockUserInteractionを呼び出す必要があります。
@implementation MyDocument
- (BOOL)canAsynchronouslyWriteToURL:(NSURL *)url ofType:(NSString *)typeName
forSaveOperation:(NSSaveOperationType)saveOperation
{
return YES;
}
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError
{
[self unblockUserInteraction];
return [NSArchiver archivedDataWithRootObject:currentObject];
}
- (IBAction)editModelObject:(id)sender
{
self.currentObject = [currentObject edit];
[[self undoManager] setActionName:@"Edit"];
}
- (void)setCurrentObject:(Model*)object
{
[[self undoManager] registerUndoWithTarget:self
selector:@selector(setCurrentObject:) object:currentObject];
currentObject = object;
}
canAsynchronouslyWriteToURL:ofType:forSaveOperation:でYESを返すことによって、dataOfType:error:がメインスレッドとは別のスレッドで呼ばれるようになります。ただし、このときメインスレッドは、このスレッドにjoinするような形でブロックされているため、この時点ではまだ非同期ではありません。unblockUserInteractionを呼ぶことで、メインスレッドのブロックが解除されて、ここから非同期保存が始まります。
このように、非同期にするだけなら簡単なのですが、問題は「非同期保存中に、メインスレッドで書類が編集されたらどうするか?」です。これに対する基本戦略は、「非同期になる前に、書類のスナップショットを取る」です。unblockUserInteractionが呼ばれるまで非同期にならないのは、スナップショットを取る最後のチャンスを与えるためです。
しかし、スナップショットとは、ようするに書類全体のコピーを作る処理のことです。ここであまり処理に時間を掛けると、非同期のメリットが失われてしまいます。
今回はこれらの問題に対し、イミュータブルな戦略を試してみました。つまり、NSArrayにオブジェクトを追加する際に、-[NSMutableArray addObject:]を使わず、代わりに-[NSArray arrayByAddingObject:]を使うということです。書類を編集する全てのメソッドで、新規に生成された編集後の書類オブジェクトを返すようにしています。
@interface Model
- (Model*)addNewPerson;
- (Model*)removeAtIndex:(NSUInteger)index;
- (Model*)setNameValue:(NSString*)name atIndex:(NSUInteger)index;
- (Model*)setAgeValue:(NSInteger)age atIndex:(NSUInteger)index;
- (Model*)setCommentValue:(NSString*)comment atIndex:(NSUInteger)index;
この方法だと、実質的に常時スナップショットを取っている形になるので、保存処理でいきなり非同期にしても問題ありません。これは、平行処理に強いと言われている関数型言語で標準的に行われている方法です。
さらに、このイミュータブルな戦略を使った際の二次的な効果として、Undo処理が簡素で信頼性の高いものになるというのがあります。
CocoaのUndoは、基本的には「書類の操作履歴」です。ある操作をしたとき、それとは逆の操作をすれば元に戻るという話です。しかし、この方法には少し問題もあります。まず、Undoのために必ず対となる操作を作らなければならないこと、それから、操作を操作で打ち消す場合、書類が確実に元の状態に戻る保証がないことです。例えば、「値を3で割る」という操作を「値を3倍する」という操作で打ち消そうとしても、端数処理の関係で元の値に戻らないかもしれません。
今回のUndoは「書類の状態履歴」という形で実装しています。書類を操作したときに捨てられるはずの古いスナップショットを、Undoとして登録するだけの簡単な実装です。これだと、Undoのために対になる操作を作る必要もないし、Undoすれば確実に元の状態に戻ります。
最後に、今回スナップショットにイミュータブルな戦略を採用した結果、バインドが使いにくくなることが分かりました。そもそもCocoaではミュータブルな戦略が採用されているので、イミュータブルな戦略とはやや相性が悪いのです。しかし、「非同期」や「平行処理」をキーワードにプログラムを書くなら、イミュータブルな戦略を採用した方が有利に展開するのは、今回のサンプルコードからも明らかです。
Cocoaは最初にリリースされてからもう10年以上立ちました。そろそろ時代遅れなものになりつつあるのかもしれませんね。