ちょっと話題の記事

[iOSアプリ開発] タッチでお絵かきしてみる

2013.03.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今回はiOSアプリ開発で、タッチでお絵かきができるサンプルを作ってみようと思います。
タッチしたところを線にして描画する処理を実装しますが、サンプルとはいえ、それだけだとあまりに使い勝手が悪いので、
UNDO(元に戻す)とREDO(やり直す)と、全部削除するクリア機能も実装してみます。

環境

今回の主な環境と設定は以下の通りです。

  • Xcode 4.6.1
  • iOS SDK 6.1
  • iPod touch 5th
  • ストーリボード使用
  • ARC使用

実装

まず、画面を作ります。
画面全体に描画用のキャンバスとして UIImageView を広げ、
画面の下の方に各種ボタンを配置します。

ios-touch-drawing01

次に、ヘッダーファイルを作成します。

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet UIImageView *canvas;
@property (weak, nonatomic) IBOutlet UIButton *undoBtn;
@property (weak, nonatomic) IBOutlet UIButton *redoBtn;
@property (weak, nonatomic) IBOutlet UIButton *clearBtn;

- (IBAction)undoBtnPressed:(id)sender;
- (IBAction)redoBtnPressed:(id)sender;
- (IBAction)clearBtnPressed:(id)sender;

@end

作成したら、ストーリボードから各プロパティとメソッドに接続しておきます。

最後に、実装ファイルを作成します。

#import "ViewController.h"

@interface ViewController ()
{
    UIBezierPath *bezierPath;
    UIImage *lastDrawImage;
    NSMutableArray *undoStack;
    NSMutableArray *redoStack;
}
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    undoStack = [NSMutableArray array];
    redoStack = [NSMutableArray array];
    
    // ボタンのenabledを設定します。
    self.undoBtn.enabled = NO;
    self.redoBtn.enabled = NO;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

- (IBAction)undoBtnPressed:(id)sender
{
    // undoスタックからパスを取り出しredoスタックに追加します。
    UIBezierPath *undoPath = undoStack.lastObject;
    [undoStack removeLastObject];
    [redoStack addObject:undoPath];
    
    // 画面をクリアします。
    lastDrawImage = nil;
    self.canvas.image = nil;
    
    // 画面にパスを描画します。
    for (UIBezierPath *path in undoStack) {
        [self drawLine:path];
        lastDrawImage = self.canvas.image;
    }
    
    // ボタンのenabledを設定します。
    self.undoBtn.enabled = (undoStack.count > 0);
    self.redoBtn.enabled = YES;
}

- (IBAction)redoBtnPressed:(id)sender
{
    // redoスタックからパスを取り出しundoスタックに追加します。
    UIBezierPath *redoPath = redoStack.lastObject;
    [redoStack removeLastObject];
    [undoStack addObject:redoPath];
    
    // 画面にパスを描画します。
    [self drawLine:redoPath];
    lastDrawImage = self.canvas.image;
    
    // ボタンのenabledを設定します。
    self.undoBtn.enabled = YES;
    self.redoBtn.enabled = (redoStack.count > 0);
}

- (IBAction)clearBtnPressed:(id)sender
{
    // 保持しているパスを全部削除します。
    [undoStack removeAllObjects];
    [redoStack removeAllObjects];
    
    // 画面をクリアします。
    lastDrawImage = nil;
    self.canvas.image = nil;
    
    // ボタンのenabledを設定します。
    self.undoBtn.enabled = NO;
    self.redoBtn.enabled = NO;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{    
    // タッチした座標を取得します。
    CGPoint currentPoint = [[touches anyObject] locationInView:self.canvas];
    
    // ボタン上の場合は処理を終了します。
    if (CGRectContainsPoint(self.undoBtn.frame, currentPoint)
        || CGRectContainsPoint(self.redoBtn.frame, currentPoint)
        || CGRectContainsPoint(self.clearBtn.frame, currentPoint)){
        return;
    }
    
    // パスを初期化します。
    bezierPath = [UIBezierPath bezierPath];
    bezierPath.lineCapStyle = kCGLineCapRound;
    bezierPath.lineWidth = 4.0;
    [bezierPath moveToPoint:currentPoint];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    // タッチ開始時にパスを初期化していない場合は処理を終了します。
    if (bezierPath == nil){
        return;
    }
    
    // タッチした座標を取得します。
    CGPoint currentPoint = [[touches anyObject] locationInView:self.canvas];
    
    // パスにポイントを追加します。
    [bezierPath addLineToPoint:currentPoint];
    
    // 線を描画します。
    [self drawLine:bezierPath];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    // タッチ開始時にパスを初期化していない場合は処理を終了します。
    if (bezierPath == nil){
        return;
    }
    
    // タッチした座標を取得します。
    CGPoint currentPoint = [[touches anyObject] locationInView:self.canvas];
    
    // パスにポイントを追加します。
    [bezierPath addLineToPoint:currentPoint];
    
    // 線を描画します。
    [self drawLine:bezierPath];
    
    // 今回描画した画像を保持します。
    lastDrawImage = self.canvas.image;
    
    // undo用にパスを保持して、redoスタックをクリアします。
    [undoStack addObject:bezierPath];
    [redoStack removeAllObjects];
    bezierPath = nil;
    
    // ボタンのenabledを設定します。
    self.undoBtn.enabled = YES;
    self.redoBtn.enabled = NO;
}

- (void)drawLine:(UIBezierPath*)path
{
    // 非表示の描画領域を生成します。
    UIGraphicsBeginImageContext(self.canvas.frame.size);
    
    // 描画領域に、前回までに描画した画像を、描画します。
    [lastDrawImage drawAtPoint:CGPointZero];
    
    // 色をセットします。
    [[UIColor blackColor] setStroke];
    
    // 線を引きます。
    [path stroke];
    
    // 描画した画像をcanvasにセットして、画面に表示します。
    self.canvas.image = UIGraphicsGetImageFromCurrentImageContext();
    
    // 描画を終了します。
    UIGraphicsEndImageContext();
}

@end

これでサンプルは完成です。
少し多めにコメントを書きましたので細かい説明は省いてしまいますが、
ポイントはタッチ系のイベントを取得するところかなと思います。
「touchesBegan:withEvent:」で、タッチしたときのイベントを取得し、
「touchesMoved:withEvent:」で、タッチしたまま指を動かしたときのイベントを取得し、
「touchesEnded:withEvent:」で、タッチした指を離したときのイベントを取得することができます。
あとはごにょごにょ線を引いたり、パスを保持したりしてるだけですね。。。

動作確認

試しにお絵かきをしてみます。

ios-touch-drawing02

お絵かきすることができました!!

素早く線を引くとカクカクしてしまったり、ものすごく長い線を描いていると線がくずれてきたりと問題はいくつかありますが、
それはまた今度ということで。。。

ではでは。