● Carbon視点でiPhone探求(2010/05/14)

  この記事は、MOSAの会員にのみ読むことができるデベロッパー向けの
  ウェブサイトMOSADeN Onlineのに掲載された記事です。ほぼ一ヶ月
  遅れでここに掲載されて行きます

  〜 対称画像の編集結果を保存する 〜


今回は、対称処理のパラメータ(対称方向とオフセット値)をModelオブジェクトへ保存しファイルとしてセーブします。この操作には「保存」ボタンを使う予定でしたが、実際に操作してみると「いちいちボタンを押すのは面倒!」と言うことが分かりました。よって「画像一覧」ボタンでテーブルビューへ戻る時に自動で保存するようにします。

ちなみに、今回のようなケースでの保存の取り扱いですが、もし「保存」ボタンで実行するとすれば、画像一覧へ戻る場合にはキャンセルと判断するのが妥当です。つまり、パラメータは保存しないということです。ところが、ユーザ側の立場になると、ついつい保存しないまま「画像一覧」を押してしまいます(笑)。キャンセルを許す設計にするかどうかはアプリで扱うデータ内容にもよるでしょうが、今回はキャンセル処理は省略し「画像一覧」ボタンでパラメータを保存することにします。使わなくなった「保存」ボタンは、今後の機能拡張で、対称処理済みの画像をiPhoneのフォトライブラリーへ登録するためのボタンとして使うことにします。

まず最初にModelクラスを見直し、ひとつしか用意していなかったパラメータ用インスタンス変数を2つ(md_para0とmd_para1)に増やします。変更するのはModel.hです。これらには、対称処理の上下と左右のオフセット値が代入されるので、変数の種類もNSIntegerからCGFloatに変更しておきます。また、対称処理の方向(ゼロから3)についてはmd_kindに保存することにします。

@interface Model : NSObject  <NSCoding>
{
  NSInteger   md_id;
  NSString   *md_name;
  NSString   *md_type;
  UIImage    *md_image;
  CGRect    md_rt;
  NSUInteger  md_flag;
  NSInteger   md_kind;   // 対称処理の方向
  CGFloat    md_para0;  // オフセット値(上下)
  CGFloat    md_para1;  // オフセット値(左右)
}

Modelクラスのインスタンス変数の構成を変更したら、ファイルから読み込む時に使うinitWithCoder:メソッドと、ファイルへ保存する時に使うencodeWithCoder:メソッドも拡張します。ここで注意する点は、今回のようModelクラスに手を加えてしまうと、以前に保存したファイルとの互換性(コンパチビリティー)がなくなると言うことです。よって、先んじてシミュレータ上の旧「しんぶんし」アプリを削除してから、今回分の開発を続行するようにしてください。

- (void)encodeWithCoder:(NSCoder *)coder
{
  [coder encodeObject:md_name];
  [coder encodeObject:md_type];
  [coder encodeDataObject:UIImageJPEGRepresentation( md_image,0.5 )];
  [coder encodeDataObject:[NSData dataWithBytes:&md_rt length:sizeof(CGRect)]];
  [coder encodeValueOfObjCType:@encode(NSInteger) at:&md_id];
  [coder encodeValueOfObjCType:@encode(NSUInteger) at:&md_flag];
  [coder encodeValueOfObjCType:@encode(NSInteger) at:&md_kind];
  [coder encodeValueOfObjCType:@encode(CGFloat) at:&md_para0];
  [coder encodeValueOfObjCType:@encode(CGFloat) at:&md_para1];
}

- (id)initWithCoder:(NSCoder *)coder
{  
  if( self=[super init] )
  {
    self.md_name=[coder decodeObject];
    self.md_type=[coder decodeObject];
    self.md_image=[UIImage imageWithData:[coder decodeDataObject]];
    [[coder decodeDataObject] getBytes:&md_rt length:sizeof(CGRect)];
    [coder decodeValueOfObjCType:@encode(NSInteger) at:&md_id];
    [coder decodeValueOfObjCType:@encode(NSUInteger) at:&md_flag];
    [coder decodeValueOfObjCType:@encode(NSInteger) at:&md_kind];
    [coder decodeValueOfObjCType:@encode(CGFloat) at:&md_para0];
    [coder decodeValueOfObjCType:@encode(CGFloat) at:&md_para1];
  }
  return self;
}

続いてImageViewController.mで最初に呼ばれるviewWillAppear:メソッドをオーバライドし、Modelオブジェクトに保存されていたパラメータ値(対称処理の方向、上下オフセット値、左右オフセット値)をコントロールやビューの適切なインスタンス変数にセットするようにします。これにより、対称表示ビューコントローラに替えた時点で、前回保存した対称方向とオフセット位置が復活できるわけです。

- (void)viewWillAppear:(BOOL)animated
{
  im_type.selectedSegmentIndex=im_model.md_kind; // モデルから対称方向を得る
  im_view0.sy_offset=im_view1.sy_offset=im_model.md_para0; // モデルから上下オフセット値を得る
  im_view2.sy_offset=im_view3.sy_offset=im_model.md_para1; // モデルから左右オフセット値を得る
}

逆に、編集後に「画像一覧」ボタンで一覧ビューへ戻る時には、viewWillDisappear:メソッドにおいて各種パラメータをModelオブジェクトへ設定します。そして最後にDocumentクラスのsaveメソッドを呼び出してファイル保存を実行します。こうすれば、この直後にアプリが異常終了しても、この時点までの編集結果は保証されるわけです。

- (void)viewWillDisappear:(BOOL)animated
{
  SymmetryAppDelegate  *app;
  
  [super viewDidAppear:animated];
  
  im_model.md_kind=im_type.selectedSegmentIndex; // 対称処理方向を設定  
  im_model.md_para0=im_view0.sy_offset; // 上下オフセット値を設定  
  im_model.md_para1=im_view2.sy_offset; // 左右オフセット値を設定  
  
  app=[[UIApplication sharedApplication] delegate];
  [app.ap_document save];  // ドキュメントのファイル保存
}

最後に、ImageViewController.mのdirection:メソッドを書き換えて、強制的にオフセット値にゼロを代入していた個所を削除します。これで、セグメンテッドコントロールで対称方向を切り替えても、前回に編集した状態のまま表示されることになります。

- (IBAction)direction:(UISegmentedControl *)sender
{
  NSUInteger  select;
  
  select=sender.selectedSegmentIndex;
  
  if( select==0 )    //  上
  {
    im_view0.sy_reverse=NO;
    im_view1.sy_reverse=NO;
  }
  else if( select==1 )  //  下
  {
    im_view0.sy_reverse=YES;
    im_view1.sy_reverse=YES;
  }
  else if( select==2 )  //  左  
  {
    im_view2.sy_reverse=YES;
    im_view3.sy_reverse=YES;
  }
  else          //  右
  {
    im_view2.sy_reverse=NO;
    im_view3.sy_reverse=NO;
  }  
  if( select <=1 )     //  上下
  {
    [im_base bringSubviewToFront:im_view0];
    [im_base bringSubviewToFront:im_view1];
    [im_view0 setNeedsDisplay];
    [im_view1 setNeedsDisplay];
  }
  else           //  左右
  {
    [im_base bringSubviewToFront:im_view2];
    [im_base bringSubviewToFront:im_view3];
    [im_view2 setNeedsDisplay];
    [im_view3 setNeedsDisplay];
  }
}

ところが、このままだと問題がひとつあります。デバイスの方向を切り替えると(例えば縦から横へ)設定された対称位置がズレてしまうのです。これは、上下と左右のオフセット値が、デバイス画面のピクセル数としてModelオブジェクトへ保存されているからです。つまり、デバイス方向の変更で画像表示枠(フレーム)サイズが変わると、オフセット値の意味が変わってしまう訳です。これを防ぐには、オフセット値を画像の実サイズに比率変換してから保存する必要があります。そこで、シンメトリービュー(SymmetryView.m)に、その値を保存するインスタンス変数(sy_para)をひとつ追加し、drawRect:メソッドが実行される度に変換式を通して計算し直して代入します。

@interface SymmetryView : UIView
{
  UIImage    *sy_image;   //  対称処理を実行する画像
  NSUInteger  sy_type;    //  対称表示方向タイプ
  BOOL     sy_reverse;   //  対称表示を反対方向へ
  CGFloat    sy_offset;    //  対称オフセット値(画面ピクセル数)
  CGFloat    sy_para;    //  対称オフセット値(実寸サイズ)
}

- (void)drawRect:(CGRect)rect
{
  CGRect      srt,frt,drt;
  CGContextRef  context;
  CGImageRef    cgimage;
  
  context=UIGraphicsGetCurrentContext();
  cgimage=sy_image.CGImage;
  srt.size=sy_image.size;
  frt=self.bounds;  
  fitBounds( 1,&frt,&srt,&drt );
  if( sy_type <=1 )
  {
    drt.origin.y=frt.origin.y;
    if( sy_reverse==YES )
      drt.origin.y+=sy_offset;
    else
      drt.origin.y-=sy_offset;
    sy_para=sy_offset*srt.size.height/drt.size.height; // 上下オフセット実寸へ変換値
  }
  else
  {
    drt.origin.x=frt.origin.x;
    if( sy_reverse==YES )
      drt.origin.x+=sy_offset;
    else
      drt.origin.x-=sy_offset;
    sy_para=sy_offset*srt.size.width/drt.size.width; // 左右オフセットを実寸へ変換
  }
  if( ( ( sy_type==0 || sy_type==3 ) && sy_reverse==YES ) ||
    ( ( sy_type==1 || sy_type==2 ) && sy_reverse==NO ) )  
  {
    CGContextTranslateCTM( context,0.0,rect.size.height );  
    CGContextScaleCTM( context,1.0,-1.0 );
  }
  else if( ( sy_type==3 && sy_reverse==NO ) || ( sy_type==2 && sy_reverse==YES ) )
  {
    CGContextTranslateCTM( context,rect.size.width,rect.size.height );
    CGContextScaleCTM( context,-1.0,-1.0 );
  }
  CGContextDrawImage( context,drt,cgimage );
}

続いて、オフセットの実寸サイズからデバイス画面上のピクセル数を得る(逆変換用)getOffset:メソッドも作成します。

- (CGFloat)getOffset:(CGFloat)para
{  
  CGRect  srt,frt,drt;
  CGFloat  offset;
  
  srt.size=sy_image.size;
  frt=self.bounds;  
  fitBounds( 1,&frt,&srt,&drt );
  if( sy_type <=1 )
    offset=para*drt.size.height/srt.size.height; // 上下オフセットのピクセル数
  else
    offset=para*drt.size.width/srt.size.width; // 左右オフセットののピクセル数
  return offset;
}

そして、ImageViewController.mのviewWillAppear:メソッドの最後の2行を以下の様に書き換えます。これにより、Modelオブイジェクトとは実寸のオフセット値がやり取りされることになり、最大精度のパラメータをファイルへ保持できるようになります。

im_view0.sy_offset=im_view1.sy_offset=[im_view0 getOffset:im_model.md_para0]; // 上下オフセット値を画面ピクセル数へ変換
im_view2.sy_offset=im_view3.sy_offset=[im_view2 getOffset:im_model.md_para1]; // 左右オフセット値を画面ピクセル数へ変換

次に、 viewWillDisappear: メソッドの真ん中の2行も以下の様に書き換えます。

im_model.md_para0=im_view0.sy_para; // 上下 オフセット値の実寸を設定  
im_model.md_para1=im_view2.sy_para; // 左右 オフセット値の実寸を設定  

最後にデバイスの回転が起こった場合に呼ばれるメソッドに、現在のオフセット実寸値を画面ピクセル数に変換する処理を追加してやります。これでデバイスを回転しても、対称位置がズレる現象はなくなりました。

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration  // デバイスの回転が起こった
{
  im_view0.sy_offset=im_view1.sy_offset=[im_view0 getOffset:im_view0.sy_para]; // 上下オフセット値を画面ピクセルへ変換
  im_view2.sy_offset=im_view3.sy_offset=[im_view2 getOffset:im_view2.sy_para]; // 左右オフセット値を画面ピクセルへ変換
  [self direction:im_type]; // 再描画
}

次回は、今回解説することができなかったアバウトの横表示にチャレンジする予定です。また、デバイスの横表示と縦表示に関係する幾つかのトピックスも紹介いたします。

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