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を用意。
今回は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などを使ってボタン有効性の表示を切り替えたり、長押しで早送り可能にするなど色々やりこみ要素があります。
この記事が導入の助けになれば幸いです。