MILLEN BOX

音楽好きの元組み込みソフトエンジニアによるプログラミング(主にiOSアプリ開発)の勉強の記録

Swiftでお絵描きアプリを作成する(第3回Redo/Undoの実装) [Array][swift2.2]

前回に引き続きお絵描きアプリを作成しようと思います。 作成するお絵描きアプリの機能は以下!

  • 拡大が可能!(前々回済み)
  • ペンの色、太さが変更可能!(前回済み)
  • Redo/Undoが可能!(今回)
  • 描いた絵の保存が可能!(次回以降)
     
    前々回はお絵描き部分の実装、前回はペンの色、太さの変更可能にしました。
    今回はRedo/Undoを可能にしたいと思います。
    前回、前々回をまだ読んでいないというあなたは以下もチェックしてみて下さい。

anthrgrnwrld.hatenablog.com

anthrgrnwrld.hatenablog.com

 
Githubは以下です。

github.com

f:id:anthrgrnwrld:20160720191801g:plain

Redo/Undoの仕組み

今回のRedo/Undoの方法はタッチ毎にUIImageの配列を保存しておいて、Undoを押した時には一つ前のUIImageを表示し、Redoが押された時にはUndo実行前のUIImageを表示する、という方法を取りたいと思います。
もう少し高度な方法としては、描画した座標を配列に保存しておくというのもありますが、今回は少しお手軽にUIImageの保存の方法を取ります。
この方がお絵描きアプリの仕組みとしては理解が深まる気がしますし!
 
流れは以下のような感じです。
 
1. Undo/Redoボタンを追加する
2. タッチの回数を保存する 3. UIImageを配列に保存する
4. Undoボタンを押した時に配列から適したUIImageを呼び出す
5. Redoボタンを押した時に配列から適したUIImageを呼び出す
 
こうやって書くとそんなに難しくは見えないですね!詳細手順は以下を参照!

1. Undo/Redoボタンを追加する

いつものようにStoryboard上にUIbuttonを追加します。 そしてそこからControlを押しながらViewをビューっと伸ばして下さい。 今回は「Undo」、「Redo」の2つのボタンを追加します。

/**
 Undoボタンを押した時の動作
 Undoを実行する
 */
@IBAction func pressUndoButton(sender: AnyObject) {
}
    
/**
 Redoボタンを押した時の動作
 Redoを実行する
 */
@IBAction func pressRedoButton(sender: AnyObject) {
}

 
ここでちょっとテストをしてみましょう。
前回のソースからちょっと変更をかけてみます。
お絵描きされる時に実行される関数drawGesture()にてstateが.Beganの時にその瞬間のself.canvasView.imageを変数に保存し、Undoを押した時にそのUIImageがself.canvasView.imageに戻される実装を行ってみます。

var saveImage: UIImage?                 //Undo/Redo用にUIImageを保存
/**
 draw動作
 */
func drawGesture(sender: AnyObject) {
        
    ...
    switch drawGesture.state {
    case .Began:
        saveImage = self.canvasView.image                           //UIImageを保存する
    ...
        
}

/**
 Undoボタンを押した時の動作
 Undoを実行する
 */
@IBAction func pressUndoButton(sender: AnyObject) {
        
    self.canvasView.image = saveImage   //保存している直前imageに置き換える
        
}

実行してみましょう。
どうです?1回だけですがUndoが実行できました。
要はこれを何回も実行できるように、UIImageの配列に履歴を保存してあげればいいわけです。

2. タッチの回数を保存する

配列に履歴の画像を保存することを前提に、現在表示中の画像のindexを知るネタとしてタッチの回数を保存します。

var currentDrawNumber = 0                //現在の表示しているは何回めのタッチか
/**
 draw動作
 */
func drawGesture(sender: AnyObject) {
        
    switch drawGesture.state {
    case .Began:
        ...
            
    case .Changed:
        ...
            
    case .Ended:
        currentDrawNumber += 1
    }
        
}

3. UIImageを配列に保存する

グローバルな配列saveImageArrayを作成し、タッチ完了毎に描画の画像を保存してみましょう。

var saveImageArray = [UIImage]()        //Undo/Redo用にUIImageを保存
/**
 draw動作
 */
func drawGesture(sender: AnyObject) {
        
    switch drawGesture.state {
    case .Began:
        ...
            
    case .Changed:
        ...
            
    case .Ended:
        currentDrawNumber += 1
        saveImageArray.append(self.canvasView.image!)               //配列にcanvasView.imageを保存
    }
        
}

タッチ完了時にnumberDrawとsaveImageArrayのメンバー数の間に矛盾がないか確認するために、以下のようなコードを追記しておいて下さい。
必須ではありませんがオススメです。この後の説明は以下が追記されていること前提に話を進めます。

if currentDrawNumber != saveImageArray.count - 1 {
    fatalError("index Error")
}

4. Undoボタンを押した時に配列から適したUIImageを呼び出す

Undoボタンを押した時に配列から適したUIImageを呼び出します。
現在表示中のindexはcurrentDrawNumberになりますので、そこから-1したものを表示します。
そして現在表示中の配列indexが変更されましたのでcurrentDrawNumberを-1にします。

/**
 Undoボタンを押した時の動作
 Undoを実行する
 */
@IBAction func pressUndoButton(sender: AnyObject) {
        
        
    self.canvasView.image = saveImageArray[currentDrawNumber - 1]   //保存している直前imageに置き換える
        
    currentDrawNumber -= 1
        
}

プロジェクトを実行し、何回が書き書きしてみて下さい。
そしてUndoを実行してみて下さい。
できましたか?できましたね?
そうしたならばさらにUndoを連打してみて下さい。
...アプリが落ちましたね?
これは「Arrayを読み込もうとしたらアプリが落ちてアレーッ」状態になっています。
currentDrawNumberが0の時には読み込む配列のindexがマイナスになってしまいますので、頭で if currentDrawNumber <= 0 {return} を実行させ0以下の時にはreturnしてあげます。
 

/**
 Redoボタンを押した時の動作
 Redoを実行する
 */
@IBAction func pressUndoButton(sender: AnyObject) {
    if currentDrawNumber <= 0 {return}    
        
    self.canvasView.image = saveImageArray[currentDrawNumber - 1]   //保存している直前imageに置き換える
        
    currentDrawNumber -= 1
        
}

 
上記問題は解決したら次の問題に進みます。
プロジェクトを実行し、何回が書き書きしてみて下さい。
その後Undoを行い、そして再度お絵描きを実行してみて下さい。
...アプリが落ちましたね?
これは手順3で追記したfatalErrorの部分が効いています。
お絵描きを実行した時にはもうRedoを考慮しなくても良いため、currentDrawNumberより後に保存されたUIImageを全て削除し、その配列の最後にappendするように実装しましょう。
そうすればfatalErrorになる条件には入らず、矛盾なく配列を追加していけます。
 

/**
 draw動作
 */
func drawGesture(sender: AnyObject) {
        
    switch drawGesture.state {
    case .Began:
        ...
            
    case .Changed:
        ...
            
    case .Ended:

        //currentDrawNumberとsaveImageArray配列数が矛盾無きまでremoveLastする
        while currentDrawNumber != saveImageArray.count - 1 {
            saveImageArray.removeLast()
        }

        currentDrawNumber += 1
        saveImageArray.append(self.canvasView.image!)               //配列にcanvasView.imageを保存
    }
        
}

5. Redoボタンを押した時に配列から適したUIImageを呼び出す

ここまでくればRedoの方法は簡単でしょう。
Undoとは逆にcurrentDrawNumberに対応するindexから+1したものを表示します。
以下のソースを参照して下さい。
因みに頭の if currentDrawNumber + 1 > saveImageArray.count - 1 {return} を実行しないと、また「Arrayを読み込もうとしたらアプリが落ちてアレーッ」状態になりますのでご注意を!

/**
 Redoボタンを押した時の動作
 Redoを実行する
 */
@IBAction func pressRedoButton(sender: AnyObject) {
        
    if currentDrawNumber + 1 > saveImageArray.count - 1 {return}
        
    self.canvasView.image = saveImageArray[currentDrawNumber + 1]   //保存しているUndo前のimageに置き換える
        
    currentDrawNumber += 1
        
}

お絵描きの仕組みを理解していればそんなに理解するのは難しくないと思います。
残り1回!次回はUIImageのカメラロールへの保存について書きます!

(続き書きました!)

anthrgrnwrld.hatenablog.com