Cocoaにおける状態の保存と復元その2

以前に「 Cocoaにおける状態の保存と復元 」にて書いた記述が間違っていた。

splitViewの中のviewをrestoreStateWithCoder:のメソッドの中で生成していたがそれはおそらく間違い。

restore stateの機能は、以下の2点を守れば小細工は必要ない。

  • view階層はrestoreStateWithCoder:が呼ばれる前に構築済みである。
  • restore sateに参加させるobjectはwindow内でユニークなidentifierが設定されている。

この2点を守るのは、通常のアプリケーションでは問題にならない。

Cocoaにおける状態の保存と復元 」で書いた様に、NSTabViewのタブやNSSplitViewのsplitが動的に増える場合にのみ問題になる。

この記事での方法上記の2点を守ることでより簡潔に「状態の保存と復元」を実装する。

実装の概要

nibファイルからのWindowの構築が終わり、メソッドrestoreStateWithCoder:が呼ばれる前のタイミングを得る場所は、以下に示す2つのメソッドになる。

- (void)restoreDocumentWindowWithIdentifier:(NSString *)identifier
                                      state:(NSCoder *)state
                          completionHandler:(void (^)(NSWindow *, NSError *))completionHandler;
- (void) makeWindowControllers;

ただし、makeWindowControllersの方は、引数としてNSCoderが渡されないために、動的にViewを構築するための情報が足らない。

よって、restoreDocumentWindowWithIdentifier:〜をオーバーライドすることになる。

通常、restoreDocumentWindowWithIdentifier:〜をオーバーライドする場合は、以下の様なコードになる。

ググっても”通常”のrestoreDocumentWindowWithIdentifierの使用方法はでてこないので私を信じてほしい。

- (void)restoreDocumentWindowWithIdentifier:(NSString *)identifier
                                      state:(NSCoder *)state
                          completionHandler:(void (^)(NSWindow *, NSError *))completionHandler
{
    NSWindowController* theWindowController;

    if( [identifier isEqualToString:@"window_01"] )
    {
        theWindowController = [self makeWindowController_01];
    }
    else if( [identifier isEqualToString:@"window_02"] )
    {
        theWindowController = [self makeWindowController_02];
    }
    .
    .
    .
    else if( [identifier isEqualToString:@"window_nn"] )
    {
        theWindowController = [self makeWindowController_nn];
    }

    completionHandler([theWindowController window], nil);
}

identifierはクラスではない。同じクラスであっても異なる用途であれば異なるidentifierが設定される。主にnibファイルによって設定され、このなるWindowControllerによって制御される。

このメソッドの中での、completionHandlerによって、restoreStateWithCoder:が呼び出される。

引数identifierとcompletionHandlerは使用されるが、引数stateは使用されていない。

引数stateに保存されている値が保存されているのは、[NSWindowController encodeRestorableStateWithCoder:]で保存できる。

状態の保存時に、動的に生成された部分の情報を[NSWindowController encodeRestorableStateWithCoder:]で保存して、[NSDocument restoreDocumentWindowWithIdentifier]で再生すると実現できる。

サンプルコードの実装

状態の保存に関してみるべきメソッドは以下の3つ。

[TVSDocument restoreDocumentWindowWithIdentifier:state:completionHandler:];
[TVSWindowController prebuildForRestorableStateWithWindowCoder:];
[TVSWindowController encodeRestorableStateWithCoder:];

restoreDocumentWindowWithIdentifierと、prebuildForRestorableStateWithWindowCoderは状態の再生時に使用している。

encodeRestorableStateWithCoderは状態の保存時に使用する。

保存再生する値はsplitViewの分割数で、”numOfSplit”と名前をつけている。

restoreDocumentWindowWithIdentifierの関連箇所は以下の様に、WindowzControllerを作成したら、stateを引数にprebuildForRestorableStateWithWindowCoderを呼び出すだけ。

- (void)restoreDocumentWindowWithIdentifier:(NSString *)identifier
                                      state:(NSCoder *)state
                          completionHandler:(void (^)(NSWindow *, NSError *))completionHandler
{
    TVSWindowController* theWindowController;

    theWindowController = (TVSWindowController*)[self makeWindowController];

    // restore state のコードが走り出す前に、View構造を構築する必要がある。
    // そのため、ここで追加のView構造を構築する機会をwindowに与える。
    [theWindowController prebuildForRestorableStateWithWindowCoder:state];

    completionHandler([theWindowController window], nil);
}

引数stateの中に、”numOfSplit”の値が入っている。ので、prebuildForRestorableStateWithWindowCoderではそれを使ってsplitViewへ分割をお願いする。

- (void) prebuildForRestorableStateWithWindowCoder:(NSCoder*)coder
{
    TVSSplitViewController* theSplitViewController = (TVSSplitViewController*) self.contentViewController;

    // restore state のコードが走り出す前に、View構造を構築する必要がある。
    NSInteger theSplitNumber = [[coder decodeObjectForKey:@"numOfSplit"] integerValue];

    [theSplitViewController createPaneWithNumber:theSplitNumber];
}

状態の保存時に関連するのは、encodeRestorableStateWithCoderだけ。restoreStateWithCoderと対になっていないので気持ち悪いが動的なViewの生成に「状態の保存」が対応していないので仕方がない。 コードとしては以下の様になっている。

- (void) encodeRestorableStateWithCoder:(NSCoder *)coder
{
    TVSSplitViewController* theSplitViewController = (TVSSplitViewController*) self.contentViewController;

    [super encodeRestorableStateWithCoder:coder];

    [coder encodeObject:[NSNumber numberWithInteger:theSplitViewController.splitViewItems.count]
              forKey:@"numOfSplit"];

    [coder encodeObject:self.indexes forKey:@"indexes"];
}

@”indexes”は選択範囲の保存に関係する箇所。画面分割に関係するのは@”numOfSplit”の箇所です。

なお、画面の分割後のsplitViewの中のviewの記述は特に工夫を要する箇所はない。

スクロール位置の保存と再生も”contentView.bounds”を保存して再生時には、reflectScrolledClipView:を使って画面をリフレッシュしているくらいです。

その他

前回のCocoa勉強会で話題になった、「選択位置の情報はどのオブジェクトが保持するべきか?」の問題ですが、WindowControllerに持たせることにしました。

これで、モデルデータは共通だが、選択位置が異なるWindowが実現できています。

なお、ソースコードはここです。

( TableViewSpliter_2.zip )