● Carbon視点でiPhone探求(2009/04/05)

  このニュースは、MOSAの会員にのみ配布されているデベロッパー向けの
  デジタルマガジンMOSADeNのに掲載された記事です。ほぼ一ヶ月遅れで
  ここに掲載されて行きます

 〜 iPhoneでもNSCodingプロトコルのお世話に 〜


今回から本格的にモデルコントローラクラスを実装していきます。まずは、モデルのアーカイブ化を行うために、NSCodingプロトコルに準拠したencodeWithCoder:とinitWithCoder:の2つのメソッドを用意することから始めましょう。

以前に定義したModelクラスをもう一度見てみます。スーパークラスとして定義されているNSObjectのさらに右側に<NSCodeing> と表記されていますが、これが、このクラスがNSCodingプロトコルに準拠していることを示しています。

@interface Model : NSObject <NSCoding>
{
  NSString    *md_name;
  NSString    *md_type;
  UIImage     *md_image;
  CGRect     md_rt;
  NSUInteger   md_flag;
  NSInteger    md_kind;
  NSInteger    md_para;
}

NSCodingプロトコルに準拠させるためには、 ModelクラスにencodeWithCoder:とinitWithCoder:の両メソッドを実装する必要があります。これは、Mac OS X用のCocoaアプリケーションとまったく同じです。具体的には、ModelオブジェクトからNSDataへ変換するアーカイブ処理(エンコード)と、NSDataからModelオブジェクトへ戻すアンアーカイブ処理(デコード)の両方を実現することになります。

まず最初は、encodeWithCoder:メソッドです。Modelクラスのインスタンス変数それぞれに対してエンコード処理を行い、6つすべてに実行すれば処理は終了となります。エンコード対象のクラスにNSCodingプロトコルが実装されていれば、NSCoderクラスのencodeObject:メソッドを使うことができ、対象オブジェクトのencodeWithCoder:メソッドをが呼び出されることで処理が芋ズル式に進みます。この処理は、ドキュメント(ファイル)を外部記憶装置に保存する時に実行します。つまり、NSMutableArrayに保存されているModelオブジェクトがアーカイブ化される時に、このメソッドが逐次呼び出されるわけです。

- (void)encodeWithCoder:(NSCoder *)coder
{
  [coder encodeObject:md_name];
  [coder encodeObject:md_type];
  [coder encodeValueOfObjCType:@encode(CGRect) at:&md_rt];
  [coder encodeValueOfObjCType:@encode(NSUInteger) at:&md_flag];
  [coder encodeValueOfObjCType:@encode(NSInteger) at:&md_kind];
  [coder encodeValueOfObjCType:@encode(NSInteger) at:&md_para];
}

ここで注意しなければいけない点は、UIImageオブジェクトはエンコードしていないことです。今回インスタンス変数のUIImage *md_imageは、UITableViewへ画像サムネイル(小画像)を表示するために用意したものです。しかし、それを一緒にドキュメントへ保存することはしません。ドキュメントをロードした時点で、別の個別ファイルから逐次サムネイルを読み込む仕組みにします。どちらにしろ、UIImageクラスはNSCodingプロトコルに準拠していないので、この方法でUIImageオブジェクトをアーカイブ化することは無理なのです。

あるクラスがNSCodingプロトコルに準拠しているかどうかを判断するには、そのクラスが定義されているヘッダファイル(UIImage.h)を見てみます。すると、クラス定義の@interface行に、対応しているプロトコル名が列記されていることが分かります。例えば、NSString.hであれば...

@interface NSString : NSObject <NSCopying, NSMutableCopying, NSCoding>

と記載されています。しかし、UIImage.hの方は以下の様になっています。

@interface UIImage : NSObject

つまりNSStringクラスには、NSCopying、NSMutableCopying、NSCodingの3つのプロトコルが実装されていますが、UIImageクラスの方には、対応プロトコルがひとつも無いことを示しています。

整数(NSIntegerやNSUInteger)や構造体(CGRect)などの非オブジェクトのエンコードには、encodeValueOfObjCType:at:メソッドを使います。メッセージとして渡す@encode()は、エンコードされた内容(種類)をC文字列で返すコンパイラ指示子です。例えば、@encode(NSInteger)なら"i"に、@encode(float)なら "f"となります。構造体の@encode(CGRect)の場合には、"{?=ffff}"(4つの浮動小数点)というC文字列となります。@encodeには、sizeof()演算子の引数として使用できる任意の型を指定できるわけです。

続いて、デコードのためのinitWithCoder:メソッドです。 こちらも、NSCodingプロトコル準拠のオブジェクト(NSStringなど)については、NSCoderクラスのdecodeObject:メソッドを使います。そうでない整数等は、decodeValueOfObjCType:at:メソッドを使いデコードを実現します。

- (id)initWithCoder:(NSCoder *)coder
{
  if( self=[super init] )
  {
    self.md_name=[coder decodeObject];
    self.md_type=[coder decodeObject];
    [coder decodeValueOfObjCType:@encode(CGRect) at:&md_rt];
    [coder decodeValueOfObjCType:@encode(NSUInteger) at:&md_flag];
    [coder decodeValueOfObjCType:@encode(NSInteger) at:&md_kind];
    [coder decodeValueOfObjCType:@encode(NSInteger) at:&md_para];

    if( [md_name length] )
    {
      // ここで画像をサムネイルへ読み込む処理がを行う。
    }
  }
  return self;
}

この時、デコード順序がエンコード順序と違ってしまうと処理は正しく行われませんので、くれぐれも注意しましょう。ドキュメント読み込み時に同時に実行したい逐次処理は、このルーチン内で行うと便利です(エラー処理は少々難ありですが...)。今回の場合であれば、画像フィルを読み込みサムネイル用のUIImageを作成する処理がそうです。

今回の方法でドキュメントのセーブとロードを実行すると、後からドキュメントフォーマットを変更するのが難しくなります。そうした事態を考慮し、先んじてModelオブジェクトのインスタンス変数に将来利用しそうなオプションパラメータ(リザーブ)を用意しておくのも手です。 また、 ドキュメントフォーマットがバージョンアップされることを考慮し、どこか別ファイル(環境設定ファイルなど)にドキュメントのバージョン番号も保持しておいた方が良いでしょう。

ところで、Modelクラスのインスタンス変数はプロパティ宣言されています。プロパティの「属性」が(retain)であるmd_nameとmd_typeのデコード値の代入は、以下の様にselfにドット演算子でつなげて記述します。

self.md_name=[coder decodeObject];
self.md_type=[coder decodeObject];

これにより、コンパイラが用意したアクセッサメソッド経由で値を代入したのと同じ意味となります。 これを以下の様にダイレクトに代入してしまうと、得られたNSStringがautoreleaseされますので、ある時点でエラーが発生しアプリケーションが落ちます。この記述の場合には自分でretainすることが必要です。くれぐれも注意いたしましょう!

md_name=[coder decodeObject];
md_type=[coder decodeObject];

また、ModelクラスのスーパークラスはNSObjectなので、先んじて以下の様な処理を実行していますが...

if( self=[super init] )

もし、このスーパークラスがNSObjectではなくNSCodingプロトコルに準拠した別のクラスであれば、上記の部分は以下のように書き換える必要があります。こちらも注意が必要です。

if( self=[super initWithCoder:coder] )

次回は、Modelオブジェクトのドキュメント(ファイル)への書き出しと読み込み処理を実装してみます。また、「しんぶんし 3」で、どのように画像ファイルを取り扱うのかも考えてみたいと思います。

copyright 2009 Ottimo, Inc. All rights reserved
無断転載・引用禁止