3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【iOS】自由にUndo/Redoできるテキスト入力欄を作る

Posted at

UndoManagerを使ってUITextViewにUndo/Redo機能を追加し、保存タイミングの制御も行う例を紹介します。

※ ここで使うUndoManagerは特にUITextView専用というものではなく、UITextFiledその他何にでも使えます
※ 「保存タイミングの制御」とは「1文字ずつ保存」「変換中は記録しない」「10秒に1度保存」などのことです

コード例

import UIKit
class MemoEditorViewController: UIViewController {
    // [1] UndoManagerと「直前の内容」(Undo時に戻したい内容)
    var textUndoManager: UndoManager = UndoManager()
    private var editingText: NSAttributedString = NSAttributedString()

    // [2] UITextViewとUndo/Redoボタン
    @IBOutlet weak var textArea: UITextView! {
        didSet { textArea.delegate = self }
    }
    @IBAction func tapUndoButton(_ sender: Any) {
        undo()
    }
    @IBAction func tapRedoButton(_ sender: Any) {
        redo()
    }

    // [5] Undo/Redoの呼び出し
    private func undo() {
        textUndoManager.undo()
        editingText = textArea.attributedText
    }
    private func redo() {
        textUndoManager.redo()
        editingText = textArea.attributedText
    }
    // [3] 直前の内容をUndo登録し、現在の内容をRedo登録する
    func registerUndo(text: NSAttributedString) {
        if textUndoManager.isUndoRegistrationEnabled {
            textUndoManager.registerUndo(withTarget: self, handler: { _ in
                if let currentText = self.textArea.attributedText { self.registerUndo(text: currentText) }
                self.textArea.attributedText = text
            })
        }
    }
}
extension MemoEditorViewController: UITextViewDelegate {
    // [4] 条件を満たした時にUndoを登録
    func textViewDidChange(_ textView: UITextView) {
        if (textView.markedTextRange == nil) {
            registerUndo(text: editingText)
            editingText = textArea.attributedText
        }
    }
}

[1] UndoManagerと「直前の内容」(Undo時に戻したい内容)

Undo/Redoの記録には、UndoManagerに「現在の内容」と「直前の内容」を渡す必要があります。
ので、UndoManagerと「直前の内容」の変数を用意しましょう。
「現在の内容」はUITextViewから直接とれるので不要。

    var textUndoManager: UndoManager = UndoManager()
    private var editingText: NSAttributedString = NSAttributedString()

[2] UITextViewとUndo/Redoボタン

Undo/Redo用ボタン2つとUITextViewを用意。
image.png
今回はStoryboardを使うのでOutlet。
ついでにTextViewのdelegateをセットし、ボタンの呼び出しundo() / redo()も仮で書いておきましょう。

    @IBOutlet weak var textArea: UITextView! {
        didSet { textArea.delegate = self }
    }
    @IBAction func tapUndoButton(_ sender: Any) {
        undo()
    }
    @IBAction func tapRedoButton(_ sender: Any) {
        redo()
    }

[3] 直前の内容をUndo登録し、現在の内容をRedo登録する

UndoManagerに、「直前の内容」に戻すというUndoを登録します。
さらにUndo時にそのUndo(つまりRedo)を登録します。ここでは「現在の内容」に戻すという操作です。
https://developer.apple.com/documentation/foundation/undomanager

    // 引数には「直前の内容」を渡す
    func registerUndo(text: NSAttributedString) {
        if textUndoManager.isUndoRegistrationEnabled {
             // Undo時にやることを登録
            textUndoManager.registerUndo(withTarget: self, handler: { _ in
                 // Undo中のUndo登録はRedo登録となる(「現在の内容」に戻すという操作を登録)
                if let currentText = self.textArea.attributedText { self.registerUndo(text: currentText) }
                 // 「直前の内容」に戻す
                self.textArea.attributedText = text
            })
        }
    }

[4] 条件を満たした時にUndoを登録

Undoを登録する条件を決めます。
この例はUITextViewDelegateを使い、内容が変更されたら登録するというオーソドックスな方法です。
保存したら「直前の内容」も更新します。

    func textViewDidChange(_ textView: UITextView) {
        if (textView.markedTextRange == nil) {
            registerUndo(text: editingText)
            editingText = textArea.attributedText
        }
    }

markedTextRange == nilは「テキストが選択状態でない」ですが、ここでは「日本語などの変換中ではない」という意味合いで使っています。
これで変換中はUndoを保存しません。

registerUndo()を他の方法で呼び出せば自由なUndo登録が可能です。
ボタンを設置したり、10秒に1回にしたり、変更カウンターを作って10溜まったらなど。

[5] Undo/Redoの呼び出し

Undo/Redoボタンを完成させます。
UndoManagerのundo() / redo()を呼ぶだけ。このとき「直前の内容」も更新。

    private func undo() {
        textUndoManager.undo()
        editingText = textArea.attributedText
    }
    private func redo() {
        textUndoManager.redo()
        editingText = textArea.attributedText
    }

UndoManagerが動作そのものを記録/呼出する仕様であったお陰で、呼び出しはえらく簡単に終わりました。

おわりに

他にもcanUndoなどを使ってボタン有効性の表示を切り替えたり、長押しで早送り可能にするなど色々やりこみ要素があります。
この記事が導入の助けになれば幸いです。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?