今回は、上下方向のシンメトリー表示だけでなく、左右方向のシンメトリー表示にもチャレンジしてみます。また、画像の上下や左右の関係を逆転(逆方向)する機能も追加してみます。
まず最初にInterface BuilderでImageViewController.xibに左右対称に使うシンメトリービューを2つ追加し、それぞれを新たに用意したim_view2とim_view3のIBOutletにコネクトします。
@interface ImageViewController : UIViewController
{
IBOutlet SymmetryView *im_view0; // シンメトリービュー 上下 Type=0
IBOutlet SymmetryView *im_view1; // シンメトリービュー 上下 Type=1
IBOutlet SymmetryView *im_view2; // シンメトリービュー 左右 Type=2
IBOutlet SymmetryView *im_view3; // シンメトリービュー 左右 Type=3
IBOutlet UIBarButtonItem *im_save; // 保存ボタン
IBOutlet UISegmentedControl *im_type; // タイプグメント
Model *im_model; // 操作対象モデル
}
今回の様にビューが重なるような複雑なレイアウトでは、オブジェクトウィンドウのアイコン表示の代わりにリスト表示を利用すると便利です。

続いてImageViewController.mのviewDidLoadメソッドを拡張します。新しく追加したシンメトリービューのim_view2とim_view3にも画像をセットします。そして、各ビューをビューコントローラのビュー(土台)に配置します。この時、対称表示のデフォルトは「上下方向」なので、先んじて2つの左右方向のシンメトリービューをaddSubview:するようにしてください(先の追加が下層となる)。加えて「上下」と「左右」を選択するセグメンテッドコントロール(UISegmentedControl)のselectedSegmentIndexプロパティ値をゼロ(上下)に設定します。将来的には、こうした初期値はアプリの環境設定に保存できるようにする予定です。
- (void)viewDidLoad
{
[super viewDidLoad];
im_view0.sy_type=0; // 下部の画像表示用(上下方向)
im_view0.sy_image=[im_model loadImage]; // 画像を読み込む
im_view1.sy_type=1; // 上部の画像表示用(上下方向)
im_view1.sy_image=im_view0.sy_image;
im_view2.sy_type=2; // 左部の画像表示用(左右方向)
im_view2.sy_image=im_view0.sy_image;
im_view3.sy_type=3; // 右部の画像表示用(左右方向)
im_view3.sy_image=im_view0.sy_image;
[self.view addSubview:im_view3]; // 左右を先に配置する
[self.view addSubview:im_view2];
[self.view addSubview:im_view0];
[self.view addSubview:im_view1];
im_type.selectedSegmentIndex=0; // ディフォルトは上下方向
self.navigationItem.rightBarButtonItem=im_save; // 保存ボタン追加
im_type.tintColor=[UIColor grayColor]; // タイプセグメントカラー
}
次は、セグメンテッドコントロールをタップした時のアクションメソッドを用意します。処理は、selectedSegmentIndexプロパティ値により切り分けます。処理内容は、表示すべき2つのシンメトリービューをbringSubviewToFront:メソッドによりすべてのビューの前方へ持ってくるだけです。
- (IBAction)direction:(UISegmentedControl *)sender
{
if( sender.selectedSegmentIndex==0 ) // 上下方向
{
im_view0.sy_offset=im_view1.sy_offset=0.0;
[self.view bringSubviewToFront:im_view0];
[self.view bringSubviewToFront:im_view1];
[im_view0 setNeedsDisplay];
[im_view1 setNeedsDisplay];
}
else // 左右方向
{
im_view2.sy_offset=im_view3.sy_offset=0.0;
[self.view bringSubviewToFront:im_view2];
[self.view bringSubviewToFront:im_view3];
[im_view2 setNeedsDisplay];
[im_view3 setNeedsDisplay];
}
}
direction:アクションメソッドの実装が終わったら、Interface Builderでセグメンテッドコントロールとコネクトします。コネクト時に選択するアクションの種類(イベント)は「Touch Down」ではなく「Value Changed」ですので御注意ください。

続いて、シンメトリービュー表示で呼ばれるdrawRect:メソッド(SymmetryView.m)に、左右方向の対称表示処理を追加します。
- (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;
drt.origin.y-=sy_offset;
}
else // 左右方向
{
drt.origin.x=frt.origin.x;
drt.origin.x-=sy_offset;
}
if( sy_type==1 || sy_type==2 ) // 上部(上下方向)と 右部(左右方向)
{
CGContextTranslateCTM( context,0.0,rect.size.height );
CGContextScaleCTM( context,1.0,-1.0 );
}
else if( sy_type==3 ) // 左部(左右方向)
{
CGContextTranslateCTM( context,rect.size.width,rect.size.height );
CGContextScaleCTM( context,-1.0,-1.0 );
}
CGContextDrawImage( context,drt,cgimage );
}
最後はタッチイベントのハンドラを修正します。まずはタップ開始のイベントハンドラです。ダブルタップで対称位置を初期化します。処理はselectedSegmentIndexプロパティ値により切り分けます。今回から最初のタップ位置を得る処理は、タップ移動中のイベントハンドラに統合しました。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event // タップ開始
{
NSUInteger ct;
ct=[[touches anyObject] tapCount];
if( ct==2 ) // ダブルタップでリセット
{
if( im_type.selectedSegmentIndex==0 ) // 上下方向
{
im_view0.sy_offset=im_view1.sy_offset=0.0;
[im_view0 setNeedsDisplay];
[im_view1 setNeedsDisplay];
}
else // 左右方向
{
im_view2.sy_offset=im_view3.sy_offset=0.0;
[im_view2 setNeedsDisplay];
[im_view3 setNeedsDisplay];
}
}
}
こちらはタップ移動中のイベントハンドラです。この処理もselectedSegmentIndexプロパティ値により切り分けられます。現在のタップ位置(CGPoint)はlocationInView:メソッドで、直前タップ位置は previousLocationInView:メソッドで得ることができます。
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event // 移動中
{
CGPoint vpt1,vpt2;
CGFloat off;
for( UITouch *touch in touches )
{
vpt1=[touch previousLocationInView:self.view];
vpt2=[touch locationInView:self.view];
break;
}
if( im_type.selectedSegmentIndex==0 ) // 上下方向
{
off=im_view0.sy_offset+vpt2.y-vpt1.y;
if( off >=0.0 )
{
im_view0.sy_offset=im_view1.sy_offset=off;
[im_view0 setNeedsDisplay];
[im_view1 setNeedsDisplay];
}
}
else // 左右方向
{
off=im_view2.sy_offset+vpt2.x-vpt1.x;
if( off >=0.0 )
{
im_view2.sy_offset=im_view3.sy_offset=off;
[im_view2 setNeedsDisplay];
[im_view3 setNeedsDisplay];
}
}
}

これで左右方向の対称処理が可能になりました。次は画像を逆方向に表示する機能を追加します。まず、今までの「リセット」ボタンを「逆方向」ボタンに変更します。「リセット」はダブルタップで代用できるので、今回から外しました。 SymmetryView.hに、逆方向モードであることを保持するためのsy_reversプロパティを用意します。
@interface SymmetryView : UIView
{
UIImage *sy_image; // 対称処理を実行する画像
NSUInteger sy_type; // 対称表示方向タイプ
BOOL sy_reverse; // 対称表示を反対方向へ
CGFloat sy_offset; // 対称オフセット値
}
以下のreverse:が「逆方向」ボタンをタップした時のアクションメソッドです。ボタンをタップするごとに方向を切り替えるトグル処理となります。
- (IBAction)reverse:(id)sender
{
if( im_view0.sy_reverse==YES ) // 逆方向
{
im_view0.sy_reverse=NO;
im_view1.sy_reverse=NO;
im_view2.sy_reverse=NO;
im_view3.sy_reverse=NO;
}
else // 通常方向
{
im_view0.sy_reverse=YES;
im_view1.sy_reverse=YES;
im_view2.sy_reverse=YES;
im_view3.sy_reverse=YES;
}
[im_view0 setNeedsDisplay];
[im_view1 setNeedsDisplay];
[im_view2 setNeedsDisplay];
[im_view3 setNeedsDisplay];
}
最後に「逆方向」ボタン機能に対応するために、シンメトリービューのdrawRect:メソッドを改良して作業完了です。
- (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;
}
else // 左右方向
{
drt.origin.x=frt.origin.x;
if( sy_reverse==YES ) // 逆方向
drt.origin.x+=sy_offset;
else
drt.origin.x-=sy_offset;
}
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 );
}

今の表示の仕組みだと、対称画像が画面最大に表示されず、あまり見栄えが良くありません。次回はこの表示の仕組みと、タッチユーザインターフェースのチューニングを行いたいと思います。