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

単純な例では、カテゴリNSRestorableStateのrestorableStateKeyPathsで保存する変数のパスを返すだけ。 NSViewControllerのサブクラスにでも以下のようなコードを書けば良い。

+ (NSArray *)restorableStateKeyPaths
{
    return [@"color"];// 実際には[]の前に@がつく。コードパーサーがバグってるので@を外した
}

これで、復帰時にプロパティcolorの内容が復元できる。

同じ動作をするものを、少し複雑に書くと。以下のようなコードになる。

@property (atomic, copy) NSColor* color;
.
.
.
- (void) encodeRestorableStateWithCoder:(NSCoder *)coder
{
    [super encodeRestorableStateWithCoder:coder];

    NSData* theData=[NSArchiver archivedDataWithRootObject:self.color];

    [coder encodeObject:theData forKey:@"color"];

}

- (void) restoreStateWithCoder:(NSCoder *)coder
{
    [super restoreStateWithCoder:coder];

    NSData* theData = [coder decodeObjectForKey:@"color"];

    if (theData != nil)
    {
        self.color =(NSColor *)[NSUnarchiver unarchiveObjectWithData:theData];
    }
}

@synthesize color = _color;

- (void) setColor:(NSColor*) inColor
{
    @synchronized(self)
    {
        _color = [inColor copy];
    }

    [self invalidateRestorableState];
}

- (NSColor*) color
{
    @synchronized(self)
    {
        return _color;
    }
}

初期設定Windowやパレットの復帰は、https://www.bignerdranch.com/blog/cocoa-ui-preservation-yall/ のコードが参考になる。

Document-Baseアプリケーションで、1個のドキュメントに複数のウインドウが開けるPhotoShopのようなアプリケーションの場合は以下のようになる。少し違うが、http://mikeabdullah.net/lion-restore-multi-window-document.html も参考になる。

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

    theWindowController = [self makeWindowController];

    completionHandler([theWindowController window], nil);
}


- (NSWindowController*) makeWindowController
{
    NSStoryboard*       theStoryboard;
    NSWindowController* theWindowController;

    theStoryboard       = [NSStoryboard storyboardWithName:@"Document"
                                                    bundle:nil];

    theWindowController = [theStoryboard instantiateInitialController];

    [self addWindowController:theWindowController];

    return theWindowController;
}

ここまでは、アップルのドキュメントなどの通り。

Windowは動的に増える事を前提にしているので、restoreDocumentWindowWithIdentifier:〜なメソッドが用意されていた。 NSTabViewのタブやNSSplitViewのsplitが動的に増える場合にはどうするか?

これは、基本に戻り、encodeRestorableStateWithCoder:やrestoreStateWithCoder:のメソッドをオーバーライドする事になる。

- (void) encodeRestorableStateWithCoder:(NSCoder *)coder
{
    [super encodeRestorableStateWithCoder:coder];

    NSMutableArray* theArray = [NSMutableArray array];

    for(NSInteger i = 0; i <= [self countOfDivider]; i++)
    {
        CGFloat thePosition = [self positionOfDividerAtIndex:i];

        [theArray addObject:@(thePosition)];
    }

    [coder encodeObject:theArray
                 forKey:@"splitSizes"];
}


- (void) restoreStateWithCoder:(NSCoder *)coder
{
    [super restoreStateWithCoder:coder];

    NSArray* theSplitSizes = [coder decodeObjectForKey:@"splitSizes"];

    // 個数分分割して
    NSInteger theSplitCount = theSplitSizes.count;

    while(theSplitCount)
    {
        [self addLastSplitItem:nil];
        theSplitCount--;
    }

    // 幅を揃える
    for(NSInteger i = 0; i < [self countOfDivider]; i++)
    {
        CGFloat thePosition = [[theSplitSizes objectAtIndex:i] floatValue];

        [self.splitView setPosition:thePosition
                   ofDividerAtIndex:i];
    }
}

- (IBAction) addLastSplitItem:(id)sender
{
    NSStoryboard*     theStoryboard;
    NSViewController* theViewController;
    NSSplitViewItem*  theItem;

    theStoryboard     = [NSStoryboard storyboardWithName:@"MainViewController" bundle:nil];
    theViewController = [theStoryboard instantiateInitialController];
    theItem           = [NSSplitViewItem splitViewItemWithViewController:theViewController];

    [self addSplitViewItem:theItem];

    [self invalidateRestorableState];
}

- (NSInteger) countOfDivider
{
    NSInteger theCount = self.splitViewItems.count;

    return (theCount == 0 ) ? 0 : theCount - 1;
}

- (void)setPosition:(CGFloat)position ofDividerAtIndex:(NSInteger)dividerIndex
{
    NSAssert( dividerIndex >= 0, nil);
    NSAssert( dividerIndex < [self countOfDivider], nil);

    [self.splitView setPosition:position
               ofDividerAtIndex:dividerIndex];
}

- (CGFloat)positionOfDividerAtIndex:(NSInteger)dividerIndex
{
    NSAssert( dividerIndex >= 0, nil);
    NSAssert( dividerIndex <= [self countOfDivider], nil);

    CGFloat thePosition = 0;

    for(NSInteger i = 0; i <= dividerIndex; i++)
    {
        NSSplitViewItem* theItem = [self.splitViewItems objectAtIndex:i];
        NSRect theFrame = theItem.viewController.view.frame;

        thePosition += theFrame.size.height;

        // 2個目のitem以降にはdividerThicknessを含める
        if( i != 0)
        {
            thePosition += self.splitView.dividerThickness;
        }
    }

    return thePosition;
}

じゃあね。