内容简介:注: 本教程基于 Xcode 10 和 iOS 12。金无足赤,人无完人。只要你实现了 UndoManager,你的用户也没有必要做完人。UndoMananger 为 app 提供了一种简单的 undo/redo 机制。通过让事物逐步“局部化”,你还能在一定程度上减少在推断中偶然出现的缺陷。
注: 本教程基于 Xcode 10 和 iOS 12。
金无足赤,人无完人。只要你实现了 UndoManager,你的用户也没有必要做完人。
UndoMananger 为 app 提供了一种简单的 undo/redo 机制。通过让事物逐步“局部化”,你还能在一定程度上减少在推断中偶然出现的缺陷。
在本教程中,你将编写一个 People Keeper 的 app,使用 Swift 的值类型改善你的局部推断(local reasoning),学习如何通过改进局部推断来实现完美的 undo/redo。
注:本教程假设你拥有中级 iOS 和 Swift 开发基础。如果你刚开始学习 iOS / Swift 开发,请先阅读我们的 《教你用 Swift 编写 iOS app》系列教程。
通过 Dowload Materials 按钮下载教程代码。编译运行 app:
这个 app 中有一些你可能会遇到并记住的人。点击 Bob、Joa 或 Sam,你将在 cell 下面看到他们的体征、喜好、忌讳。
点击 PeopleListViewContoroller(左图)中的 Bob,会打开 PersonDetailViewController(右图)。右边一系列截图显示了 PersonDetailViewController 的 scroll view 中的内容。
要理解示例代码,请浏览项目文件并仔细阅读其中的注释。添加、编辑联系人的工作就留给你自己去完成了。
修改 app
如果 Sam 剃掉了胡子、Joan 戴起了眼镜怎么办?又或者,在那个特别寒冷的冬天,Bob 突然对那个冬天中的每一样东西都不喜欢了怎么办?在真实背景中,能够修改 People Keeper 中的人物是非常有用的。
实现选中操作
首先,如果在 PersonDetailViewController 中选择某个新特性,那么预览页应该随之改变。为此,在 PersonDetailViewController 的 UICollectionViewDelegate 和 UICollectionViewDataSource 扩展中添加代码:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { // 1 switch Section(at: indexPath) { // 2 case .hairColor: person.face.hairColor = Person.HairColor.allCases[indexPath.row] case .hairLength: person.face.hairLength = Person.HairLength.allCases[indexPath.row] case .eyeColor: person.face.eyeColor = Person.EyeColor.allCases[indexPath.row] // 3 case .glasses: person.face.glasses = true // 4 case .facialHair: person.face.facialHair.insert(Person.FacialHair.allCases[indexPath.row]) // 5 case .likes: person.likes.insert(Person.Topic.allCases[indexPath.row]) person.dislikes.remove(Person.Topic.allCases[indexPath.row]) case .dislikes: person.dislikes.insert(Person.Topic.allCases[indexPath.row]) person.likes.remove(Person.Topic.allCases[indexPath.row]) default: break } // 6 collectionView.reloadData() }
当 cell 被选中,需要做如下动作:
- 在 Switch 语句中,根据不同的 section 执行不同的 case 分支。
- 如果用户选择的是头发颜色,则根据 index path 的 row 来改变人物的头发颜色为 Person.HairColor。如果用户选择了头发长度或者眼睛颜色,则设置的就是头发长度或眼睛颜色。
- 当用户点击眼睛,那么该人物的 glasses 属性就变成 true。
- facialHair 是一个 Set 集合,因为它包含许多选项。当用户选择某个胡须类型时,就会添加到这个集合中。
- 当用户从喜好和忌讳中选中某一项时,则添加到对应的 likes 或 dislikes 集合。同时,一样东西不可能同时在 likes 和 dislikes 中同时存在,因此当用户喜欢某样东西时,这样东西就会从 disklikes 中移除,反之亦然。
- 刷新 Collection view ,更新预览和选中内容的 UI。
实现反选操作
接下来,实现反选操作。在 collectionView(_:didSelectItemAt:) 之下,添加:
// 1 override func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { switch Section(at: indexPath) { case .facialHair, .glasses, .likes, .dislikes: return true default: return false } } override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { switch Section(at: indexPath) { // 2 case .facialHair: person.face.facialHair.subtract([Person.FacialHair.allCases[indexPath.row]]) case .likes: person.likes.subtract([Person.Topic.allCases[indexPath.row]]) case .dislikes: person.dislikes.subtract([Person.Topic.allCases[indexPath.row]]) case .glasses: // 3 person.face.glasses = false default: break } collectionView.reloadData() }
在上面的委托方法中:
- 这里,定义只有当胡须、眼睛、喜好和忌讳被选中后才能通过再次点击来反选。而其它 section 只有当用户选择了同一类的其它 item 时才会被反选。
- 当用户反选了胡须、喜好或忌讳后,将该项从对应的 set 中移除。
- 当用户反选眼镜时,将 glasses 设置为 false。
Build & run app。现在你会看到选中后的效果:
现在你已经是一个 People Keeper 技术的高手了。你可以去炫耀这款新式武器了。当某天有个厉害的开发者看到这个 app 时,发现可以用 foundation 中的一个强大的类来保护你的市场地位,抵抗那些邪恶的竞争者……
UndoManager 介绍
UndoManager 是一个通用的 undo 栈,用于简化 app 的状态管理。它可以保存你想保存的任何对象或 UI 状态,通过一个闭包、方法或 invocation ,你可以跟踪和回溯这些状态。如果实现正确,它很容易实现 undo/redo 功能,但经验比较少的开发者很可能在实现 UndoManager 时导致致命的错误。下面两个 undo 栈例子中,一个是有问题的,一个是正确的。
Undo 栈示例 1
Undo 栈 #1 是一系列小步骤,每一步都会修改模型并让视图保持一致。尽管这种策略理论上是可行的,但是随着操作列表的增长,出错的可能性也会增加,因为精确地匹配模型中的每一个变更到视图中会越来越困难。
要理解这个,请做一个练习:
-
当你第一次 pop undo 操作栈之后,模型会变成什么样?
答案:Bob,Sam
-
第二次 undo 之后呢?
答案:Bob, Kathy
-
第三次之后呢?
答案:Bob, Kathy, Mike
无论你的结果是什么,你都可以想象得到,反复删除和插入操作多次后,后续的插入、删除、更新需要对索引进行的计算有多复杂。undo 栈是依赖于顺序的,顺序错误会导致数据模型和视图不一致。这个错误有点眼熟吧:
Undo 栈示例 2
要避免上面的错误,就不要将数据模型和 UI 变更分开记录,而是记录整个模型:
要撤销一个操作,你可以用 undo 栈中的模型替换当前模型。Undo 栈 1 和 栈 2 做同样的工作,但栈 2 是不依赖顺序的,同时出错的可能性更小。
undo 详情视图
在 PersonDetailViewController.swift 底部加入:
// MARK: - Model & State Types extension PersonDetailViewController { // 1 private func personDidChange(from fromPerson: Person) { // 2 collectionView?.reloadData() // 3 undoManager.registerUndo(withTarget: self) { target in let currentFromPerson: Person = self.person self.person = fromPerson self.personDidChange(from: currentFromPerson) } // 4 // Update button UI DispatchQueue.main.async { self.undoButton.isEnabled = self.undoManager.canUndo self.redoButton.isEnabled = self.undoManager.canRedo } } }
上面的代码执行了以下步骤:
- personDidChange(from:) 使用之前版本的 person 作为参数。
- 刷新 collection view,更新预览和单元格选中状态。
- undoManager 注册了一个 undo 操作,当进行撤销操作时,将 self.person 设置为上一次操作的 person,然后递归调用 personDidChange(from:) 方法。personDidChange(from:) 会更新 UI,然后又注册 undo 的 undo …,这样就为 undo 过的操作注册了一个 redo 路径。
- 如果 undoManager 能够进行 undo 操作 ——即 canUndo 为 true,那么 enable undo 按钮 —— 否则,disable 它。redo 按钮也是同样的。如果代码在主线程中运行,undo mananger 不会更新状态,除非方法 return。通过 DispatchQueue 块让 UI 刷新等到 undo/redo 操作完成。
然后,在 collectionView(_:didSelectItemAt:) 和 collectionView(_:didDeselectItemAt:) 的头部添加:
let fromPerson: Person = person
保存一份原 person 的实例。
在这两个委托方法的最后,将 collectionView.reloadData() 替换为:
personDidChange(from: fromPerson)
这样就注册了一个能恢复到 fromPerson 的 undo 操作。我们将collectionView?.reloadData() 删除,是因为在personDidChange(from:) 已经调用了它,没有必要调用两次。
在 undoTapped() 方法中加入:
undoManager.undo()
然后在 redoTapped() 中添加:
undoManager.redo()
分别用于进行 undo 和 redo 操作。
实现摇晃手势
接下来,实现通过摇晃设备触发 undo/redo。在 viewDidAppear(_:smiley: 底部添加:
becomeFirstResponder()
在 viewWillDisappear(_:smiley: 底部添加:
resignFirstResponder()
在 viewWillDisappear(_:smiley: 后面添加:
override var canBecomeFirstResponder: Bool { return true }
当用户摇晃手机进行 undo/redo 时,NSResponder 会在 responder 链中查找下一个能够返回 NSUndoManager 对象的 reponder。当你将 PersonDetailViewController 设置为 first responder 后,它的 undoManager 会负责响应摇晃手势,并用一个 option 表示 undo/redo 操作。
Build & run。切换到 PersonDetailViewController,改变头发颜色,然后点击 undo/redo 或者摇晃手机。
注意点击 undo/redo 时不会改变预览图。
来 debug 一下,在 registerUndo(withTarget:handler:) 闭包开头加上:
print(fromPerson.face.hairColor) print(self.person.face.hairColor)
再次 build & run。修改头发颜色多次,undo 然后 redo。现在,注意 debug 控制台,你会看到,在 undo/redo 时,两个打印语句都只输出了最终选择的那个颜色。是 UndoManager 出错了吗?
NO! 这个问题是其它代码导致的。
改善局部推理性
局部推理性是一个概念,它能够不依赖于上下文理解代码的片段。
例如在本教程中,你使用了闭包,懒加载、协议扩展和精简代码路径来使你的一部分代码易于理解,那就不需要再去阅读它们的范围之外的代码了——只需要阅读“局部的”代码。
那怎么解决这个 bug 呢?你可以用提升局部推理性来修改这个 bug。通过理解引用类型和值类型之间的区别,你会知道怎样让你的代码拥有更好的局部控制能力。
引用类型和值类型
在 Swift 中,引用类型和值类型是两种不同的“类型”。对于引用类型,比如一个类,对同一实例的不同引用将共享同一内存。值类型不同——比如 struct、enum 和 tuple —— 它们每一个实例都拥有独立的数据。
要理解这对你面临的难题有什么用,请尝试用你刚学的引用类型与值类型之间的区别来回答下列问题:
-
如果 Person 是一个类:
var person = Person() person.face.hairColor = .blonde var anotherPerson = person anotherPerson.face.hairColor = .black
问:person.face.hairColor == ??
答案:.black
-
如果 Person 是一个 struct:
var person = Person() person.face.hairColor = .blonde var anotherPerson = person anotherPerson.face.hairColor = .black
问:person.face.hairColor == ??
答案:.blonde
有问题的引用会损害局部推理,因为对象的值可能在你的控制下发生变化,在没有上下文的情况下是不能使用的。
因此在 Person.swift 中,将 Person 类修改为:
struct Person {
这样 Person 就变成值类型了,拥有单独的内存。
Build & run。然后,修改人物的特征,undo 然后 redo,看看有什么变化:
undo 和 redo 选项现在工作正常了。
然后,为 name 的修改添加 undo/redo 能力。回到 PersonDetailViewController.swift ,在 UITextFieldDelegate 扩展中添加:
func textFieldDidEndEditing(_ textField: UITextField) { if let text = textField.text { let fromPerson: Person = person person.name = text personDidChange(from: fromPerson) } }
当编辑完 text field 后,将新 name 设置给 Person,然后注册 undo 操作。
Build & run。现在,进行名字、特征的修改,undo、redo 等等。大部分功能都正常,但有一个小问题。如果你选择 name 字段,然后按返回键,不进行任何编辑,undo 按钮会激活,说明有一个 undo 操作被注册到 undoManager 中了,虽然你根本未进行任何修改:
为了解决这个问题,你需要对原 name 和新 name 进行比对,只有二者值不同时才注册 undo。但这样做的局部推理就很差了——尤其是当人物的属性列表变大的时候,比较简单的做法是比较整个 person 对象而非比对单一属性。
在 personDidChange(from:) 一开始添加:
if fromPerson == self.person { return }
理论上,这是对老对象和新对象进行了比较,但实际上却会报错:
Binary operator '==' cannot be applied to operands of type 'Person' and 'Person!'
正如它所说,Person 对象并没有内置的 compare 方法,因为其中有几个属性是自定义类型。你必须自己定义比较方法。幸好,struct 有一个简单的解决方法。
让 struct 变成 Equatable 的
回到 Person.swift ,添加一个扩展,让它遵守 Equatable:
// MARK: - Equatable extension Person: Equatable { static func ==(_ firstPerson: Person, _ secondPerson: Person) -> Bool { return firstPerson.name == secondPerson.name && firstPerson.face == secondPerson.face && firstPerson.likes == secondPerson.likes && firstPerson.dislikes == secondPerson.dislikes } }
现在,如果两个 Person 的名字、面孔、喜好、忌讳相等,那么他们相等,否则不等。
注:你可以对 Face 和 Topic 对象使用 ==(_:_:smiley:,而无需让他们实现 Equatable,因为他们仅仅是由 String 构成的对象,而 String 在 Swift 中本来就是 Equatable 对象。
回到 PersonDetailViewController.swift。Build & run。if fromPerson == self.person 上的错误将消失。现在你的这句代码 ok 了,待会还要完全删除它。用一个 diff 取代它,将有利于提升你的局部推理。
创建 diff
在编程语言中,diff 用于比较两个对象是否不同或有多不同。通过创建一个 diff 值类型,可以将(1) 原对象、(2) 修改过的对象、(3) 以及它们的比较方法都放在一个单一的、“局部”的地方。
在 Person 结构体最后添加:
// 1 struct Diff { let from: Person let to: Person fileprivate init(from: Person, to: Person) { self.from = from self.to = to } // 2 var hasChanges: Bool { return from != to } } // 3 func diffed(with other: Person) -> Diff { return Diff(from: self, to: other) }
上述代码定义了:
- struct Diff 保存了两个 Person,源(from)和目标(to)。
- 如果 from to 不同,hasChange 是 true,否则 false。
- diffed(with:) 会返回一个 Diff,包含了它的旧值 from 和新值 to。
在 PersonDetailViewController,将 private func personDidChange(from fromPerson: Person) { 替换为:
private func personDidChange(diff: Person.Diff) {
现在参数变成了 Diff 而非仅仅 from 对象。
然后,将 if fromPerson == self.person { return } 换成:
guard diff.hasChanges else { return }
利用了 diff 的 hasChanges 属性。
同时删除先前添加的两句 print 语句。
提升代码的相邻性
在将 personDidChange(from:) 替换为 personDidChange(diff:) 之前,先来看一眼 collectionView(_:didSelectItemAt:) 和 collectionView(_:didDeselectItemAt:) 方法。
在每个方法中,注意 person 对象在类一开始就保存了原始值,但到最后也没有用到它。你可以通过将这个对象的创建移到更近的地方,来提升代码的局部推理性。
在同一扩展的 personDidChange(diff:) 方法之前添加方法:
// 1 private func modifyPerson(_ mutatePerson: (inout Person) -> Void) { // 2 var person: Person = self.person // 3 let oldPerson = person // 4 mutatePerson(&person) // 5 let personDiff = oldPerson.diffed(with: person) personDidChange(diff: personDiff) }
上述代码解释如下:
- modifyPerson(_:smiley: 使用一个闭包作为参数,该闭包接收一个 Person 对象指针。
- var person 保存这个类的当前 Person 对象的可变副本。
- oldPerson 保存一个原 person 对象的常量引用
- 调用 (inout Person) -> Void 闭包,这个闭包是调用 modifyPerson(_:smiley: 时传入的。这句代码负责修改 person 变量。
- personDidChange(diff:) 方法更新 UI 并注册 undo 操作,恢复到 fromPerson 数据模型对象。
在 collectionView(_:didSelectItemAt:)、collectionView(_:didDeselectItemAt:) 和 textFieldDidEndEditing(_:smiley: 方法中调用 modifyPerson(_:smiley:,将 let fromPerson: Person = person 替换为:
modifyPerson { person in
将 personDidChange(from: fromPerson) 替换为:
}
这样就将代码放到了 modifyPerson(_:smiley: 闭包中。
同样,在 undoManager 的 registerUndo 闭包中,将 let currentFromPerson: Person = self.person 替换成:
target.modifyPerson { person in
将 self.personDidChange(from: fromPerson) 替换成:
}
这样代码就被一个闭包简化了。这种设计方式将修改代码集中到一处,保证我们 UI 的局部推理性。
选中类中所有代码,点击菜单 Editor > Structure > Re-Indent 重排闭包的缩进。
在 personDidChange(diff:) 的 guard diff.hasChanges else { return } 之后、collectionView?.reloadData() 之前添加:
person = diff.to
将类的 person 属性设置为更新后的 person。
同样,在 target.modifyPerson { person in … } 闭包中,将 self.person = fromPerson 替换为:
person = diff.from
当 undo 时恢复之前的 person。
Build & run。查看人物详情视图,每一样功能都正常了。你的 PersonDetailViewController 代码已经写完了!
现在,点击 < PeopleKeeper 返回按钮。呃……我们的修改去哪里了呢?你必须将修改传给 PeopleListViewController。
修改人员名单
在 PersonDetailViewController 头部添加:
var personDidChange: ((Person) -> Void)?
和 personDidChange(diff:) 方法不同,这个 personDidChange 变量会保存一个闭包,这个闭包用修改后的 person 作为参数。
在 viewWillDisappear(_:smiley: 方法开头,添加:
personDidChange?(person)
当 view 消失,返回到主界面时,修改后的 person 会传给这个闭包。
现在需要给这个闭包赋值。
回到 PeopleListViewController, 找到 prepare(for:sender:)。当转换到人员的详情视图时,prepare(for:sender:) 会发送一个 person 对象给目标控制器。同样,你可以在这个方法中添加一个闭包,以接收从目标控制器返回的 person 对象。
在 prepare(for:sender:) 最后添加:
detailViewController?.personDidChange = { updatedPerson in // 暂时空缺: 更新数据模型和 UI }
这句代码对 detailViewController 的 personDidChange 闭包进行初始化。最终你会在占位注释的地方编写更新数据和 UI 的代码,在这之前,还有一些准备工作要做。
打开 PeopleModel.swift。在 PeopleModel 的最后、类的内部添加:
struct Diff { // 1 enum PeopleChange { case inserted(Person) case removed(Person) case updated(Person) case none } // 2 let peopleChange: PeopleChange let from: PeopleModel let to: PeopleModel fileprivate init(peopleChange: PeopleChange, from: PeopleModel, to: PeopleModel) { self.peopleChange = peopleChange self.from = from self.to = to } }
这段代码主要做了以下事情:
- Diff 中定义了一个 PeopleChange 枚举,它描述了:1)from 和 to 之间变化是插入、删除、修改还是什么也没做;2)哪一个 person 是被插入、删除或修改的 person。
- Diff 中保存了原始值和修改值(PeopleModel),以及 PeopleChange。
要计算出被插入、删除或修改的 person 到底是哪个,要在 Diff 结构体之后添加这个函数:
// 1 func changedPerson(in other: PeopleModel) -> Person? { // 2 if people.count != other.people.count { let largerArray = other.people.count > people.count ? other.people : people let smallerArray = other.people == largerArray ? people : other.people return largerArray.first(where: { firstPerson -> Bool in !smallerArray.contains(where: { secondPerson -> Bool in firstPerson.tag == secondPerson.tag }) }) // 3 } else { return other.people.enumerated().compactMap({ index, person in if person != people[index] { return person } return nil }).first } }
上述代码分解为以下几个步骤:
- changedPerson(in:) 对比 self 的当前 PeopleModel 和参数传入的 PeopleModel,然后返回被插入/删除/修改的那个 Person。
- 如果两个数组元素个数不等,找出二者中较大的一个,然后在这个数组中找出较小者中不包含的第一个元素。
- 如果两个数组元素个数相同,那么应该是修改操作而非插入或删除操作,这时,遍历新数组,找出和老数组中对应元素不同的 person。
在 changedPerson(in:) 下面添加方法:
// 1 func diffed(with other: PeopleModel) -> Diff { var peopleChange: Diff.PeopleChange = .none // 2 if let changedPerson = changedPerson(in: other) { if other.people.count > people.count { peopleChange = .inserted(changedPerson) } else if other.people.count < people.count { peopleChange = .removed(changedPerson) } else { peopleChange = .updated(changedPerson) } } //3 return Diff(peopleChange: peopleChange, from: self, to: other) }
来看一下上面的代码:
- peopleChange 先初始化为 none 表示没有变化。在方法最后会返回这个 peopoleChange。
- 如果新数组 size 大于老数组,changedPerson 是插入;如果更小是删除;如果二者 size 相等,那么是修改。在每一种情况中,用 changedPerson(in:) 返回的 person 作为 PeopleChange 的参数。
- 用 peopleChange、原始 PeopleModel、新 PeopleModle 构建一个 Diff 并返回。
然后在 PeopleListViewController.swift 最后添加:
// MARK: - Model & State Types extension PeopleListViewController { // 1 private func peopleModelDidChange(diff: PeopleModel.Diff) { // 2 switch diff.peopleChange { case .inserted(let person): if let index = diff.to.people.index(of: person) { tableView.insertRows(at: [IndexPath(item: index, section: 0)], with: .automatic) } case .removed(let person): if let index = diff.from.people.index(of: person) { tableView.deleteRows(at: [IndexPath(item: index, section: 0)], with: .automatic) } case .updated(let person): if let index = diff.to.people.index(of: person) { tableView.reloadRows(at: [IndexPath(item: index, section: 0)], with: .automatic) } default: return } // 3 peopleModel = diff.to } }
和 PersonDetailViewController 的 personDidChange(diff:) 一样,peopleModelDidChange(diff:) 方法主要做了以下工作:
- peopleModelDidChange(diff:) 使用一个 PeopleModel.Diff 参数,它根据数据模型所发生的改变来更新 UI。
- 如果 diff 的 peopleChange 类型是插入,则在 person 所在的位置插入一行。如果 peopleChange 是删除,则删除 person 所在的行。如果 peopleChange 是修改,reload 该行。否则,没有任何改变,退出方法执行,模型和 UI 都不需要更新。
- 设置 class 的 peopleModel 为更新后的模型。
刚刚在 PersonDetailViewController 添加了一个 modifyPerson(_:smiley: 方法,现在再在 peopleModelDidChange(diff:) 方法前面添加一个 modifyModel(_:smiley: 方法:
// 1 private func modifyModel(_ mutations: (inout PeopleModel) -> Void) { // 2 var peopleModel = self.peopleModel // 3 let oldModel = peopleModel // 4 mutations(&peopleModel) // 5 tableView.beginUpdates() // 6 let modelDiff = oldModel.diffed(with: peopleModel) peopleModelDidChange(diff: modelDiff) // 7 tableView.endUpdates() }
这段代码解释如下:
- modifyModel(_:smiley: 使用一个闭包参数,这个闭包接收一个可变的 PeopleModel 指针作为参数。
- var peopleModel 保存一份 peopleModel 的可变的拷贝。
- oldModel 保存原 model 的不可变的引用。
- 在老模型上执行 mutations 闭包,产生新 model。
- 开始更新 tableView。
- peopleModelDidChange(diff:) 根据 modelDiff 的 peopleDiff 负责 tableView 的插入、删除或刷新。
- tableView 更新结束。
回到 prepare(for:sender:) 方法,将占位注释替换为:
self.modifyModel { model in model.people[selectedIndex] = updatedPerson }
这会将用户所点击的索引所对应的 person 更新为修改后的版本。
最后一步。将 class PeopleModel { 替换为:
struct PeopleModel {
Build & run。选择某人的详情视图,进行某些修改,然后返回人员列表,改变会传递过来:
接着,你需要为人员列表添加删除、添加功能。
要实现删除,将 tableView(_:editActionsForRowAt:) 的占位注释替换为:
self.modifyModel { model in model.people.remove(at: indexPath.row) }
这会将指定行索引的 person 删除,无论从数据模型还是 UI 上。
要实现插入,需要添加一个 addPersonTapped() 方法:
// 1 tagNumber += 1 // 2 let person = Person(name: "", face: (hairColor: .black, hairLength: .bald, eyeColor: .black, facialHair: [], glasses: false), likes: [], dislikes: [], tag: tagNumber) // 3 modifyModel { model in model.people += [person] } // 4 tableView.selectRow(at: IndexPath(item: peopleModel.people.count - 1, section: 0), animated: true, scrollPosition: .bottom) showPersonDetails(at: IndexPath(item: peopleModel.people.count - 1, section: 0))
代码解释如下:
- 类的 tagNumber 属性记录的是 people 模型中的最大 tag 值。因为添加了新的 Person,所以 tagNumber 加 1。
- person 刚创建时没有 name、likes 和 dislikes,但是 face 采用默认值。其 tag 值等于当前的 tagNumber。
- 将 person 添加到 data model 最后,更新 UI。
- 选中新添加的行 —— 也就是最后一行 —— 并跳转到这个 person 的详情视图,以便用户编辑。
Build & run。进行添加,修改等操作。你已经可以从人员名单中添加、删除 person 了,同时改动应该能够在两个控制器之间同步:
但是还没完 —— PeopleListViewController 的 undo/redo 还不能用。是时候做一点反破坏代码来保护你的联系人列表了!
取消联系人修改
在 peopleModelDidChange(diff:) 末尾添加:
// 1 undoManager.registerUndo(withTarget: self) { target in // 2 target.modifyModel { model in model = diff.from } } // 3 DispatchQueue.main.async { self.undoButton.isEnabled = self.undoManager.canUndo self.redoButton.isEnabled = self.undoManager.canRedo }
在这里,你:
- 注册一个 undo 操作,以便撤销对数据模型和 UI 的改变。
- 修改 people 模型,用原来的值替换当前值。
- Enable/disable undo/redo 按钮。
在 undoTapped() 中加入:
undoManager.undo()
在 redoTapped() 中加入:
undoManager.redo()
分别调用了 undo 和 redo 方法。
最后,为控制器添加摇晃手势。在 viewDidAppear(_:smiley: 中添加:
becomeFirstResponder()
在 viewWillDisappear(_:smiley: 中添加:
resignFirstResponder()
在 viewWillDisappear(_:smiley: 下面添加:
override var canBecomeFirstResponder: Bool { return true }
这样控制器就能响应摇晃手势进行 undo/redo 了。
OK!Build & run。你可以编辑、添加、撤销、重做、摇晃手机了。
大功告成!
接下来去哪里
下面的 Download Materials 按钮可以下载示例代码供你参考。
要进一步了解 UndoManager API,你可以尝试一下分组撤销、对撤销动作进行命名、使撤销重做无效以及使用内置通知。
要进一步了解值类型,请尝试为 Person 和 PeopleModel 添加属性,让你的 app 更加健壮。
如果你想让 PeopleKeeper 真正能为你所用,请对数据进行持久化。更多信息,请看我们的 “Updated Course: Saving Data in iOS” 。
有任何问题、建议或意见,请到论坛发帖。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 立体字母建模教程【C4D教程】
- PS学习教程 PS制作字体发光效果教程
- 【C4D教程】卡通风可爱小乌龟建模教程
- 卡通风仙人掌建模教程【C4D教程】
- 3D立体字体制作教程,C4D建模教程
- 3D小乌龟制作教程,C4D建模教程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入剖析Tomcat
Budi Kurniawan、Paul Deck / 曹旭东 / 机械工业出版社华章公司 / 2011-12-31 / 59.00元
本书深入剖析Tomcat 4和Tomcat 5中的每个组件,并揭示其内部工作原理。通过学习本书,你将可以自行开发Tomcat组件,或者扩展已有的组件。 Tomcat是目前比较流行的Web服务器之一。作为一个开源和小型的轻量级应用服务器,Tomcat 易于使用,便于部署,但Tomcat本身是一个非常复杂的系统,包含了很多功能模块。这些功能模块构成了Tomcat的核心结构。本书从最基本的HTTP请求开......一起来看看 《深入剖析Tomcat》 这本书的介绍吧!