iOSの画像加工アプリでUndoRedoの実装 (NSCodingの話とか)


UndoRedoとは

一応知らない人の為に説明すると、Xcodeでいう[戻る(commond + z)],[進む(commond + shift + z)]のことです。
ま、イラレとかフォトショとか何にでもありますよね。

UndoRedoと書いていますが、今回説明するのはUndoです。UndoさえできればRedoも出来たようなものなので分かりやすくする為にあえて書きません。

動作動画

[do]で行った作業を[Undo]で戻っていく動画です。
動画にはありませんが、画像を追加、削除等の動きも全て戻れるように出来ています。

do

Undo

実装概要

保存方法

まず考えるべきことは、どのようにして保存するか。
戻れるということは、状態が変化するタイミングで現在の状態を保存しておく必要があり、全て残す必要があります。

配列に突っ込む(不正解)

シンプルな方法としては、配列に状態を突っ込んでいく方法があります。
しかし今回は画像の情報を保存する必要があるので、配列に突っ込んでいくと一瞬でメモリを使い切ってしまいます。
仕様上半永久的に戻れる必要があったためそれでは出来ません。

ファイルとしてデバイスに保存する(正解)

次に考える方法としては、すべての状態をファイルとして端末のドライブに保存していく方法です。
この機能では、高いレスポンスが必要ではないので、メモリを使わないこの方法が適切だと思います。

NSCoding

iosプログラミングをしていると、NSCodingという記述をちょいちょい見かけることがあるのではないでしょうか。
簡単に説明すると「インスタンスの状態をそのままファイルとして保存できる仕組みを提供してくれるクラス」です。

保存方法

NSKeyedArchiver.archiveRootObject([保存インスタンス], toFile: [保存パス])

読み込み方法

let object = NSKeyedUnarchiver.unarchiveObjectWithFile([保存パス])

そうなんです、超簡単でしょ?

これだけで、インスタンスの状態をそっくりファイルに保存できてしまいます。
基本はこれで良いのですが、自分でUIViewなどを継承して作ったクラスのプロパティなどは、少しコードを追記してあげる必要があります。
次にその説明をします。

カスタムクラスのプロパティも保存出来るようにする

例えば以下のようなクラスがあったとします。

DynamicImageView.swift
class DynamicImageView: UIView {
    private var beforePoint:CGPoint?
    private var afterPoint:CGPoint?
    private var image:UIImage?
}

何もしなければこやつらは保存されません。
・private var beforePoint:CGPoint?
・private var afterPoint:CGPoint?
・private var imageView:UIImageView?

保存させるためにはこう書きます

 バイナリに変換する処理と、バイナリから戻す処理を追記するわけですね。

DynamicView.swift
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    beforePoint = aDecoder.decodeCGPointForKey("beforePoint")
    afterPoint = aDecoder.decodeCGPointForKey("afterPoint")    
    imageView = aDecoder.decodeObjectForKey("imageView") as? UIImageView
}

override func encodeWithCoder(aCoder: NSCoder) {
    super.encodeWithCoder(aCoder)
    aCoder.encodeCGPoint(beforePoint!, forKey: "beforePoint")
    aCoder.encodeCGPoint(afterPoint!, forKey: "afterPoint")    
    aCoder.encodeObject(imageView!, forKey: "imageView")    
}

流れのおさらい

保存

NSKeyedArchiver.archiveRootObjectでオブジェクトをシリアライズするためにencodeWithCoder(aCoder: NSCoder)が呼ばれる

読み込み

NSKeyedUnarchiver.unarchiveObjectWithFileでシリアライズされたオブジェクトを復元する為にinit?(coder aDecoder: NSCoder)が呼ばれる

NSCodingを上手に使いUndoできる処理を書く

Undoさせる為には大きく分けて2つ保存しなければなりません。
 ・どういう操作を行ったか
 ・操作前のインスタンスの状態
 
私の場合は以下のように、シリアライズ専用クラスを作って、そこに保存したいものを突っ込んでこのインスタンスを保存するという形をとりました。

UndoSerializeObject.swift
 class UndoSerializeObject:NSObject, NSCoding {
    var dynamicImageView:DynamicImageView!
    var actionType:ActionType!//これenumで別で定義してます
    override init(){
        super.init()
    }

    @objc required init?(coder aDecoder: NSCoder) {
        super.init()
        self.dynamicImageView = aDecoder.decodeObjectForKey("dynamicImageView") as! DynamicImageView
        self.actionType = ActionType(rawValue: aDecoder.decodeIntegerForKey("actionType"))
    }

    @objc func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeObject(dynamicmageView, forKey: "dynamicImageView")
        aCoder.encodeInteger(actionType.hashValue, forKey: "actionType")
    }

}

UndoSerializeObjectの使用例

example.swift
let UNDO_FOLDER_PATH:String! = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] + "/undo"

func 何かしらのアクション(){
    let undoSerializeObject = UndoSerializeObject()
    undoSerializeObject.dynamicImageView = "保存したいインスタンス"
    undoSerializeObject.actionType = "アクションタイプ"
    saveUndoObject(undoSerializeObject,UNDO_FOLDER_PATH + [ファイル名 + 連番])//連番は別で管理している
    連番++
}

func 戻る(){
    連番--
    let undoSerializeObject = loadUndoObject(UNDO_FOLDER_PATH + [ファイル名 + 連番])


    //undoSerializeObject.actionTypeを見て、復元処理を行う
}

func saveUndoObject(object:UndoSerializeObject,filePath:String){
    NSKeyedArchiver.archiveRootObject(object, toFile:filePath)
}

func loadUndoObject(loadFileName filePath:String) -> UndoSerializeObject?{
    let filePath = UNDO_REDO_FOLDER_PATH + "/" + fileName
    let object = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? UndoRedoSaveObject
    deleteUndoRedo(filePath)//取り出したファイルを削除
    return object
}

まとめ

実際にundoを実装しようとするともっと沢山のプロパティを保存する必要があったり、工夫する必要がある部分が出てくるので、言葉足らずではありますが、これを参考に進めていただければ、何とか作れると思いますので頑張って下さい。

質問等あれば、追記で書きますので宜しくお願いします。