ドキュメントベースアプリにおける
2つのArrayのアーカイブ実験


mkinoさんのHMDT BBSでNorikoさんが質問されていた

> documentをアーカイブして保存、ロードする方法なんですが、1つのarrayだけを保存する方法は、
> いろいろな所に書いてあるのですが、2つのarrayを保存、ロードする時には、どうすればよいのでしょうか?
> dataRepresentationOfTypeとloadDataRepresentationをどう書けばいいかわからないのです。

を実現するサンプルを作ってみました。

基本的な方針は私がコメントしたように2つのArrayを束ねる要素数=2のルートオブジェクトとなるNSArrayを作り、そこからそれぞれのArrayをポイントさせ、そのルートオブジェクトを指定してNSKeyedArchiverでアーカイブを行うというものです。(読み込み時はその逆)

なお、データの保持、入力/表示のメカニズムに関してはCocoaバインディングを使用しています。

ただ、私は今までドキュメントベースのアプリを作ったことがなく、NSArrayController等からデータ変更時にNSDocumentのDirtyフラグ(データの変更有無)を立てる方法がわからなかったため、掲載しているサンプルでは何もしてなくても無条件にデータ変更ありと見なしてファイル保存を聞いて来るという、情けない仕様になってますがご勘弁を。(^_^;)

1.サンプルプログラムの概要

#5のNorikoさんの書き込み

> Windows上には、2つのTableViewがあります。TableViewの中身は,
> 2つのarray1,array2に入っています。array1, 2の要素は、NSMutableDictionaryでしまっています。

から以下のような画面のプログラムを作ることにしました。



2.クラス・オブジェクトの関連図

このサンプルを構成するクラス・オブジェクトの関連図を以下に示します。(クリックで拡大)



3.プロジェクトの作成とドキュメントファイルの拡張子の設定

新規プロジェクト作成で、プロジェクトのタイプに「Cocoa Document-based Application」を選択してプロジェクトを作成してください。(このサンプルでは"DocBaseTest"という名前で作成しました)

次に、このアプリケーションの扱うドキュメントファイルの拡張子の設定を行います。
とりあえずこのサンプルでは拡張子はdbatestとします。
ドキュメントファイルの拡張子の設定を行うには、プロジェクトウィンドウの左端のリストの「ターゲット」をクリックして展開し、その中にあるターゲット(このサンプルではDocAppTest)を右クリックし、「情報を見る」を選んでインスペクタを表示させます。



インスペクタ画面の「プロパティ」タブを開き、表示される設定画面の一番下にある「書類のタイプ」のリストの拡張子を今回は「dbatest」に変更します。
ちなみに「クラス」はこのタイプの書類を扱うクラス名で、「名前」は書類の読み込み/保存時に呼び出されるメソッドのパラメータとしてこの文字列が渡され、プログラム内で書類のタイプを識別するための情報です。「OSタイプ」はOS9時代からの名残の4文字のファイルタイプです。



4.Cocoaバインディング部分の作成

NSDocumentのサブクラス以外のM-V-Cの制御部分のソースやIBでの設定については「Cocoaバインディングを使ってみる」と「モデルオブジェクトをファイルへ保存する」を参照してください。

Viewの部分が書籍情報と著者情報の2つになったので、NSArrayControllerのインスタンスをそれぞれ用に用意したこと、著者情報用にUCAuthorを作ったこと(UCBookはそのまま流用)以外は基本的には同じ手順です。

なお今回追加したUCAuthorのソースは以下の通りです

UCAuthor.h:著者情報インターフェースファイル
@interface UCAuthor : NSObject <NSCoding>
{
    // インスタンス変数
    NSString    *_pstrName;            // 作者名
    NSString    *_pstrInfo;            // 付加追加情報
}

// 初期化
- (id)init;

// 後処理
- (void)dealloc;

//
// アクセサ(キー・バリュー・コーディング準拠)
//
// 氏名用
- (NSString *)name;
- (void)setName:(NSString *)pstrName;

// 付加情報用
- (NSString *)info;
- (void)setInfo:(NSString *)pstrInfo;

//
// データのArchive/Unarchive(NSCoding Protocol)
//
// 読み込み時
- (id)initWithCoder:(NSCoder *)decoder;

// 書き込み時
- (void)encodeWithCoder:(NSCoder *)encoder;

@end

UCAuthor.m:著者情報インプリメントファイル
#import "UCAuthor.h"

// ファイル内での各項目のキー
#define KEY_NAME        @"UCAKeyName"
#define KEY_INFO        @"UCAKeyInfo"

@implementation UCAuthor

//
// 初期化処理
//
- (id)init
{
    // 親クラス呼び出し
    self = [super init];
    if ( self == nil ) {
        return nil;
    }

    // 各メンバ変数初期化(新規作成時のデフォルト値)
    _pstrName   = [[NSString alloc] initWithCString:"No Name"];
    _pstrInfo   = [[NSString alloc] initWithCString:""];

    return self;
}

//
// 後処理
//
- (void)dealloc
{
    if ( _pstrName != nil ) {
        [_pstrName release];
    }
    if ( _pstrInfo != nil ) {
        [_pstrInfo release];
    }

    [super dealloc];
}

//
// 氏名取得/設定
//
- (NSString *)name
{
    return _pstrName;
}

- (void)setName:(NSString *)pstrName
{
    [_pstrName release];
    _pstrName = [pstrName retain];
}

//
// 付加情報取得/設定
//
- (NSString *)info
{
    return _pstrInfo;
}

- (void)setInfo:(NSString *)pstrInfo
{
    [_pstrInfo release];
    _pstrInfo = [pstrInfo retain];
}

//
// データのArchive/Unarchive(NSCoding Protocol)
//
// ファイルからのオブジェクト読み込み時
- (id)initWithCoder:(NSCoder *)decoder
{
    // 注:親クラスであるNSObjectはinitWithCoder:をインプリメントしていないのでinitの呼び出しだけとする
    self = [super init];

    // 各インスタンス変数の読み込み
    _pstrName   = [[decoder decodeObjectForKey:KEY_NAME] retain];
    _pstrInfo   = [[decoder decodeObjectForKey:KEY_INFO] retain];

    return self;
}

// ファイルへのオブジェクト書き込み時
- (void)encodeWithCoder:(NSCoder *)encoder
{
    // 注:親クラスであるNSObjectはencodeWithCoder:をインプリメントしていないので呼び出しは行わない

    // 各インスタンス変数の書き込み
    [encoder encodeObject:_pstrName    forKey:KEY_NAME];
    [encoder encodeObject:_pstrInfo forKey:KEY_INFO];
}

@end


5.ドキュメントクラスへの保存/読み込み処理の組み込み

Xcodeでプロジェクトのタイプにドキュメントベースアプリケーションを指定してプロジェクトを作成するとNSDocumentのサブクラスであるMyDocumentというクラスのソースファイルのスケルトンと、そのドキュメント用のnibファイル「MyDocument.nib」が自動的に作成されます。
これらに対して以下の操作を行います。

5.1 コントローラへのOutletの追加と接続

データの読み込み/保存時に、MyDocumentクラスのインスタンスがNSArrayControllerが抱えているモデルデータオブジェクトにアクセスする必要があるため、書籍情報用、著者情報用の各コントローラへのOutletを追加し、コントローラとリンクします。


●追加するアウトレット

    // NSArrayControllerへのOutlet
    IBOutlet NSArrayController *_outAuthorCtrl;        // 著者情報用
    IBOutlet NSArrayController *_outBookCtrl;          // 書籍情報用

なお、MyDocumentクラスのインスタンスはMyDocument.nibのFile's Ownerなので、コントローラとのリンクは以下のように行います。



5.2 保存処理の組み込み

Xcodeが自動的に生成するMyDocumentクラスのスケルトンには、NSDocumentのメソッドの中でサブクラスでオーバーライドすることが期待されるメソッドのひな形が含まれています。
このうち、データの保存処理では以下のメソッドに必要な処理を組み込みます。

// ドキュメント内データの指定されたドキュメントタイプでのデータ表現(NSDataオブジェクト)を生成する
- (NSData *)dataRepresentationOfType:(NSString *)aType

このメソッドは、ドキュメントクラスのオブジェクトが保持している内部形式のデータを指定されたドキュメントタイプの形式で符号化したものをNSDataオブジェクトとして通知するものです。

具体的には以下の処理を組み込みます。

- (NSData *)dataRepresentationOfType:(NSString *)aType
{
    // Insert code here to write your document from the given data.  You can also choose to override -fileWrapperRepresentationOfType: or -writeToFile:ofType: instead.

    // 2つあるArrayを1つにまとめ、NSKeyedArchiverでアーカイブ化する
    NSArray *parrContents = [NSArray arrayWithObjects:[_outBookCtrl content], [_outAuthorCtrl content], nil];
    return [NSKeyedArchiver archivedDataWithRootObject:parrContents];
}

処理内容は以下の通りです。

(1) NSArrayControllerのインスタンスをポイントしている各Outletからインスタンス内のモデルデータオブジェクトを取得する
(2) (1)で取得したオブジェクトを要素とする要素数=2のNSArrayオブジェクト(ルートオブジェクト)を生成する
(3) (2)で生成したオブジェクトを指定してNSKeyedArchiverのarchivedDataWithRootObject:メソッドでNSDateオブジェクトとして符号化する
(4) (3)で生成したNSDataオブジェクトを本メソッドの復帰値として通知する


そしてもう一つ、私の調査・知識不足でCocoaバインディングを使った時にドキュメントクラスにデータの変更があったことを通知する方法がわからなかったので、常にデータが変更されたことを通知して保存処理が実行されるように以下のメソッドをオーバーライドしています。(情けなかぁ〜 (;_;))

- (BOOL)isDocumentEdited
{
    return YES;
}

以上の処理の組み込みが終わったらビルドし、データを入力、ウィンドウを閉じてみてください。
保存ダイアログが表示され、保存ボタンをクリックすればその場所にファイルが作成され、サイズが0でなければとりあえずOKです。

5.3 読み込み処理の組み込み

事情は後で説明しますが、まず、書籍情報のArrayと著者情報のArrayを保持しておくための以下のインスタンス変数を追加し、initメソッドにその初期化処理を追加します。

    // データLoad時のワーク
    // ファイルからの非アーカイブ時点ではCocoaバインディングのNSArrayControllerはまだ初期化されておらず、contentの設定ができないので設定できるまでの間保存しておくための領域
    NSMutableArray        *_parrBook;        // 書籍情報
    NSMutableArray        *_parrAuthor;      // 著者情報

データの読み込み処理では以下のメソッドに必要な処理を組み込みます。

// NSDataオブジェクトとして読み込まれた指定されたドキュメントタイプのデータから、ドキュメントの内部形式のデータを復元する
- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType

具体的には以下の処理を組み込みます。

- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType
{
    // Insert code here to read your document from the given data.  You can also choose to override -loadFileWrapperRepresentation:ofType: or -readFromFile:ofType: instead.

    // NSKeyedUnarchiverで非アーカイブ化したArrayから書籍情報と著者情報のArrayに分解する
    NSArray *parrContents = [NSKeyedUnarchiver unarchiveObjectWithData:data];

    // NSArrayControllerの準備ができた時点でcontentとして設定できるようにワーク変数に保存しておく
    _parrBook   = [[parrContents objectAtIndex:0] retain];
    _parrAuthor = [[parrContents objectAtIndex:1] retain];

#ifdef  SET_CONTROLLER_CONTENT_WHEN_LOADING
    // ここのコードはここでNSArrayControllerにcontentの設定を行ってしまったらどうなるかを実験するために入れているものです
    [_outBookCtrl setContent:_parrBook];
    [_parrBook release];

    [_outAuthorCtrl setContent:_parrAuthor];
    [_parrAuthor release];
#endif

    return YES;
}

処理内容は以下の通りです。

(1) NSKeyedUnarchiverのunarchiveObjectWithData:メソッドでパラメータとして渡されたNSDataオブジェクトから元のオブジェクトを復元する
(2) 保存時に符号化したオブジェクトのルートオブジェクトは書籍情報と著者情報をポイントするNSArrayなので、その中からそれぞれのArrayを取出し、インスタンス変数に設定する

提示したソースでは、上で説明した処理の後に#ifdefで括られたNSArrayControllerにコンテンツとして(2)で取得したオブジェクトを設定する処理が入っていますが、実は最初、NSArrayControllerへのオブジェクトの設定処理についてはこの部分で大丈夫だろうと思って作ったのですが、動かしてみると保存したはずのデータがビューに表示されませんでした。

状況から推測して、loadDataRepresentation:ofType:が呼ばれた時点ではOutletの先のNSArrayControllerはまだ初期化されておらず、せっかくオブジェクトを設定してもその後実行される初期化処理で消されてしまうのではないかと考え、MyDocumentのスケルトンの以下のメソッドでNSArrayControllerへのオブジェクト設定処理を追加してみました。(最初に追加したインスタンス変数はこのためです)

// WindowControllerからのドキュメントウィンドウを含むnibファイルがロードされたことの通知
- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
    // Add any code here that needs to be executed once the windowController has loaded the document's window.

    // ファイルからNSKeyedUnarchiverで復元しておいたモデルデータをNSArrayControllerのcontentとして設定する
#ifndef SET_CONTROLLER_CONTENT_WHEN_LOADING
    if ( _parrBook != nil ) {
        [_outBookCtrl setContent:_parrBook];
        [_parrBook release];
        _parrBook = nil;
    }

    if ( _parrAuthor != nil ) {
        [_outAuthorCtrl setContent:_parrAuthor];
        [_parrAuthor release];
        _parrAuthor = nil;
    }
#endif
}

処理内容はloadDataRepresentation:ofType:メソッドでインスタンス変数に保存しておいた各情報のArrayオブジェクトをNSArrayControllerのコンテンツに設定することです。
#ifndefで囲ってあるのは、データのロードとコントローラの設定タイミングによる不具合を簡単に再現できるようにするためです。

以上で読み込み処理の組み込みは終了です。ビルド&実行し、メニューのOpenで先程保存したファイルを選択してみてください、保存時に入力した値が画面に復元されれば成功です。


サンプルプログラムのダウンロード(1.5MB)

【作成・確認環境】
Mac OS X v10.3.4
Xcode v1.2


一覧に戻る