在Swift中实现撤销功能

在过去的一段时间里,有很多的Blog推出了关于他们想在Swift中所添加的动态特性的文章。事实上Swift 已经成为了一门具有相当多动态特性的语言:它拥有泛型,协议, 头等函数(译者注1:first-class function指函数可以向类一样作为参数传递),和包含很多可以的动态操作的函数的标准库,比如mapfilter等(这意味着我们可以利用更安全更灵活的函数来代替 KVC 来使用 字符串)(译者注2:KVC指Key-Value-Coding一个非正式的 Protocol,提供一种机制来间接访问对象的属性)。对于大多数人而言,特别希望介绍反射这一特性,这意味着他们可以在程序运行时进行观察和修改。

Swift中,反射机制受到很多的限制,但是你仍然你可以在代码运行的时候动态的生成和插入一些东西。 比如这里是怎样为NSCoding或者是JSON动态生成字典的实例。

今天在这里,我们将一起看一下在Swift中怎样去实现撤销功能。 其中一种方法是通过利用Objective-C中基于的反射机制所提供的NSUndoManager。通过利用struct,我们可以利用不同的方式在我们的APP中实现撤销这一功能。 在教程开始之前,请务必确保你自己已经理解了Swiftstruct的工作机制(最重要的是理解他们都是独立的拷贝)。
首先要声明的一点是,这篇文章并不是想告诉大家我们不需要对runtime进行操作,或者我们提供的是一种NSUndoManager的替代品。这篇文章只是告诉了大家一种不同的思考方式而已。

我们首先创建一个叫做UndoHistorystruct。 通常而言,创建UndoHistory时会伴随一个警告,提示只有当A是一个struct的时才会生效。为了保存所有状态信息,我们需要将其存放入一个数组之中。当我们修改了什么时,我们只需要将其push进数组中,当我们希望进行撤回时,我们将其从数组中pop出去。我们通常希望有一个初试状态,所以我们需要建立一个初始化方法:

1
2
3
4
5
6
7
struct UndoHistory<A> {
private let initialValue: A
private var history: [A] = []
init(initialValue: A) {
self.initialValue = initialValue
}
}

举个例子,如果我们想在一个tableViewController中通过数组的方式提供撤销操作,我们可以创建这样一个struct

1
var history = UndoHistory(initialValue: [1, 2, 3])

对于不同情境下的撤销操作,我们可以创建不同的struct来实现:

1
2
3
4
struct Person {
var name: String
var age: Int
}

1
var personHistory = UndoHistory(initialValue: Person(name: "Chris", age: 31))

当然,我们希望获得当前的状态,同时设置当前状态。(换句话说:我们希望实时地操作我们的历史记录)。我们可以从history数组中的最后一项值来获取我们的状态,同时如果数组为空的话,我们便返回我们的初始值。 我们可以通过将当前状态添加至history数组来改变我们的操作状态。

1
2
3
4
5
6
7
8
9
10
extension UndoHistory {
var currentItem: A {
get {
return history.last ?? initialValue
}
set {
history.append(newValue)
}
}
}

比如,如果我们想修改个人年龄(译者注3:指前面作者编写的Person结构体中的age属性), 我们可以通过重新计算属性来很轻松的做到这一点:

1
2
personHistory.currentItem.age += 1
personHistory.currentItem.age // Prints 32

当然,undo 方法的编写并未完成。对于从数组中移出最后一个元素来讲是非常简单的。 根据你自己的声明,你可以在数组为空的时候抛出一个异常,不过,我没有选择这样一种做法。

1
2
3
4
5
6
extension UndoHistory {
mutating func undo() {
guard !history.isEmpty else { return }
history.removeLast()
}
}

很简单的使用它(译者注4:这里指作者前面所编写的undo相关代码)

1
2
3
personHistory.undo()
personHistory.currentItem.age // Prints 31 again
~

当然,我们到现在的UndoHistory操作只是基于一个很简单的Person类。比如,如果我们想利用Array来实现一个tableviewcontrollerundo操作,我们可以利用属性来获取从数组中得到的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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) {
guard editingStyle == .Delete else { return }
data.currentItem.removeAtIndex(indexPath.row)
}
}

struct中另一个非常爽的特性是:我们可以自由的使用监听者模式。 比如,我们可以修改data的值:

1
2
3
4
5
var data: UndoHistory<[item]> {
didSet {
tableView.reloadData()
}
}

我们即使是修改数组内很深的值(比如:data.currentItem[17].name = “John”),我们通过didSet也能很方便地定位到修改的地方。当然,我们可能希望做一些例如reloadData这样方便的事情。比如, 我们可以利用Changeset 库来计算变化,然后来根据插入/删除/移动/等不同的操作来添加动画。

很明显的是, 这种方法有着它自身的缺点。例如,它保存了整个状态的历史操作,不是每次状态变化之间的不同点。 这种方法只使用了struct来实现undo操作 (更为准确的讲:是只使用了struct中值的一些特性)。这意味着,你并不需要去阅读 runtime编程指导这本书, 你只需要对structgenerics(译者注5:generics指泛型)有足够的了解。

  1. 为data.currentItem提供了一个可计算的属性 items 来进行获取和设置操作,是一个不错的想法。这使得data-sourcedelegate等方法的实现变得更为容易。
  2. 如果你想更进一步优化,这里有一些非常有意思的想法:添加恢复功能,或者是编辑功能。你可以在tableView中去实现, 如果你真的很天真的按照这个去做了,那么你会发现在你的undo历史中会存在重复记录。
Share Comments