- 原文鏈接 : Undo History in Swift
- 原文作者 : chriseidhof
- 譯文出自 : 掘金翻譯計劃
- 譯者 : Zheaoli
- 校對者: xcc3641, Jaeger
在過去的一段時間裡,有很多的 Blog 推出了關於他們想在Swift中所添加的動態特性的文章。事實上Swift 已經成為了一門具有相當多動態特性的語言:它擁有泛型,協議, 頭等函數(譯者注 1:first-class function 指函數可以向類一樣作為參數傳遞),和包含很多可以的動態操作的函數的標準庫,比如map和filter等(這意味著我們可以利用更安全更靈活的函數來代替 KVC 來使用 字符串)(譯者注 2:KVC 指 Key-Value-Coding 一個非正式的 Protocol,提供一種機制來間接訪問對象的屬性)。對於大多數人而言,特別希望介紹反射這一特性,這意味著他們可以在程序運行時進行觀察和修改。
在Swift中,反射機制受到很多的限制,但是你仍然你可以在代碼運行的時候動態的生成和插入一些東西。 比如這裡是怎樣為NSCoding或者是 JSON 動態生成字典的實例。
今天在這裡,我們將一起看一下在Swift中怎樣去實現撤銷功能。 其中一種方法是通過利用Objective-C中基於的反射機制所提供的NSUndoManager。通過利用struct,我們可以利用不同的方式在我們的 APP 中實現撤銷這一功能。 在教程開始之前,請務必確保你自己已經理解了Swift中struct的工作機制 (最重要的是理解它們都是獨立的拷貝)。
首先要聲明的一點是,這篇文章並不是想告訴大家我們不需要對runtime進行操作,或者我們提供的是一種NSUndoManager的替代品。這篇文章只是告訴了大家一種不同的思考方式而已。
我們首先創建一個叫做UndoHistory的struct。 通常而言,創建 UndoHistory 時會伴隨一個警告,提示只有當 A 是一個 struct 的時才會生效。為了保存所有狀態信息,我們需要將其存放入一個數組之中。當我們修改了什麼時,我們只需要將其push進數組中,當我們希望進行撤回時,我們將其從數組中pop出去。我們通常希望有一個初始狀態,所以我們需要建立一個初始化方法:
struct UndoHistory<A> {
private let initialValue: A
private var history: [A] = []
init(initialValue: A) {
self.initialValue = initialValue
}
}
舉個例子,如果我們想在一個tableViewController中通過數組的方式提供撤銷操作,我們可以創建這樣一個struct:
var history = UndoHistory(initialValue: [1, 2, 3])
對於不同情境下的撤銷操作,我們可以創建不同的struct來實現:
struct Person {
var name: String
var age: Int
}
var personHistory = UndoHistory(initialValue: Person(name: "Chris", age: 31))
當然,我們希望獲得當前的狀態,同時設置當前狀態。(換句話說:我們希望實時地操作我們的歷史記錄)。我們可以從history數組中的最後一項值來獲取我們的狀態,同時如果數組為空的話,我們便返回我們的初始值。 我們可以通過將當前狀態添加至history數組來改變我們的操作狀態。
extension UndoHistory {
var currentItem: A {
get {
return history.last ?? initialValue
}
set {
history.append(newValue)
}
}
}
比如,如果我們想修改個人年齡(譯者注 3:指前面作者編寫的Person結構體中的age屬性), 我們可以通過重新計算屬性來很輕鬆的做到這一點:
personHistory.currentItem.age += 1
personHistory.currentItem.age // Prints 32
當然,undo 方法的編寫並未完成。對於從數組中移出最後一個元素來講是非常簡單的。 根據你自己的聲明,你可以在數組為空的時候拋出一個異常,不過,我沒有選擇這樣一種做法。
extension UndoHistory {
mutating func undo() {
guard !history.isEmpty else { return }
history.removeLast()
}
}
很簡單的使用它(譯者注 4:這裡指作者前面所編寫的undo相關代碼)
personHistory.undo()
personHistory.currentItem.age // Prints 31 again
當然,我們到現在的UndoHistory操作只是基於一個很簡單的Person類。比如,如果我們想利用Array來實現一個tableviewcontroller的undo操作,我們可以利用屬性來獲取從數組中得到的元素:
final class MyTableViewController<item>: UITableViewController {
var data: UndoHistory<[item]>
init(value: [Item]) {
data = UndoHistory(initialValue: value)
super.init(style: .Plain)
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.currentItem.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Identifier", forIndexPath: indexPath)
let item = data.currentItem[indexPath.row]
// configure `cell` with `item`
return cell
}
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) -> Void {
guard editingStyle == .Delete else { return }
data.currentItem.removeAtIndex(indexPath.row)
}
}
在struct中另一個非常爽的特性是:我們可以自由的使用監聽者模式。 比如,我們可以修改data的值:
var data: UndoHistory<[item]> {
didSet {
tableView.reloadData()
}
}
我們即使是修改數組內很深的值(比如:data.currentItem[17].name = "John"),我們通過didSet也能很方便地定位到修改的地方。當然,我們可能希望做一些例如reloadData這樣方便的事情。比如,我們可以利用Changeset 庫來計算變化,然後來根據插入 / 刪除 / 移動 / 等不同的操作來添加動畫。
很明顯的是,這種方法有著它自身的缺點。例如,它保存了整個狀態的歷史操作,不是每次狀態變化之間的不同點。 這種方法只使用了struct來實現undo操作 (更為準確的講:是只使用了struct中值的一些特性)。這意味著,你並不需要去閱讀 runtime編程指導這本書, 你只需要對struct和generics(譯者注 5:generics 指泛型)有足夠的了解。
- 為 data.currentItem 提供了一個可計算的屬性 items 來進行獲取和設置操作,是一個不錯的想法。這使得data-source和delegate等方法的實現變得更為容易。
- 如果你想更進一步優化,這裡有一些非常有意思的想法:添加恢復功能,或者是編輯功能。你可以在tableView中去實現,如果你真的很天真的按照這個去做了,那麼你會發現在你的undo歷史中會存在重複記錄。