委托模式使一个对象能够使用另一个 "帮助者 "对象来提供数据或执行任务,而不是自己去做这个任务。这种模式有三个部分。
一个需要委托的对象,也被称为委托对象。它是拥有委托的对象。委托通常是作为一个弱属性持有,以避免委托对象保留委托对象,而委托对象又保留委托对象的循环。
委托协议,它规定了一个委托对象可以或应该实现的方法。
委托对象,即实现委托协议的辅助对象。
通过依赖委托协议而不是具体的对象,实现起来更加灵活:任何实现了协议的对象都可以作为委托对象使用。
使用这种模式来分解大类或创建通用的、可重用的组件。委托关系在整个苹果框架中很常见,尤其是UIKit。以数据源命名的对象和以代表命名的对象实际上都遵循委托模式,因为每个对象都要求另一个对象提供数据或做一些事情。
为什么在苹果框架中不只有一个协议,而有两个?
苹果框架通常使用术语DataSource来分组提供数据的委托方法。例如,UITableViewDataSource被期望提供UITableViewCells来显示。
苹果框架通常使用名为Delegate的协议来分组接收数据或事件的方法。例如,每当有一行被选中,UITableViewDelegate就会被通知。
数据源和代理通常被设置为同一个对象,比如拥有一个UITableView的视图控制器。然而,它们不一定非得如此,有时将它们设置为不同的对象也是非常有益的。
让我们来看看一些代码!
打开Starter目录下的FundamentalDesignPatterns.xcworkspace,然后打开Overview页面,如果还没有的话。你会看到Delegation被列在Behavioral Patterns下面。这是因为委托是关于一个对象与另一个对象的交流。
点击 "委托 "链接,打开该页面。
在这个代码例子中,你将创建一个MenuViewController,它有一个tableView,同时作为UITableViewDataSource和UITableViewDelegate。
首先,创建MenuViewController类,在代码示例后直接添加以下代码,暂时忽略任何编译器错误。
import UIKit
public class MenuViewController: UIViewController {
// 1
@IBOutlet public var tableView: UITableView! {
didSet {
tableView.dataSource = self
tableView.delegate = self
}
}
// 2
private let items = ["Item 1", "Item 2", "Item 3"]
}
下面是这个的作用。
在真正的应用程序中,你还需要在Interface Builder中为tableView设置@IBOutlet,或者在代码中创建表视图。你也可以选择直接在Interface Builder中设置tableView.delegate和tableView.dataSource,或者你可以在代码中这样做,如图。
这些项目将被用作显示在表视图上的菜单标题。
正如Xcode可能抱怨的那样,你实际上需要使MenuViewController符合UITableViewDataSource和UITableViewDelegate。
在类的定义下面添加以下代码。
// MARK: - UITableViewDataSource
extension MenuViewController: UITableViewDataSource {
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = items[indexPath.row] return cell }
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
}
// MARK: - UITableViewDelegate
extension MenuViewController: UITableViewDelegate {
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// To do next....
}
}
UITableViewDataSource和UITableViewDelegate在技术上都是委托协议。它们规定了一个 "帮助 "对象必须实现的方法。
创建你自己的委托也很容易。例如,你可以创建一个委托,当用户选择一个菜单项时就会被通知。
在下面添加以下代码,导入UIKit。
public protocol MenuViewControllerDelegate: class {
func menuViewController(
_ menuViewController: MenuViewController,
didSelectItemAtIndex index: Int)
}
接下来,在@IBOutlet var tableView上面添加以下属性。
public weak var delegate: MenuViewControllerDelegate?
在iOS中常见的惯例是在一个对象被创建后设置委托对象。这正是你在这里所做的:在MenuViewController被创建后(无论这在应用程序中如何发生),它期望它的delegate属性将被设置。
最后,你需要在用户选择一个项目时实际通知这个委托。
将UITableViewDelegate扩展中的//接下来要做的...注释替换为以下内容。
delegate?.menuViewController(self, didSelectItemAtIndex: indexPath.row)
通常的惯例是,将委托对象,也就是本例中的MenuViewController,传递给其每个委托方法的调用。这样,如果需要的话,委托对象可以使用或检查调用者。
所以现在你已经创建了你自己的委托协议,当列表中的一个项目被选中时,MenuViewController将委托给它。在一个真正的应用程序中,这将处理当项目被选中时该做什么,例如移动到一个新的屏幕。
很简单,对吗?
委托是非常有用的,但它们可能被过度使用。要小心为一个对象创建太多的委托。
如果一个对象需要几个委托,这可能说明它做得太多了。考虑将对象的功能分割成特定的用例,而不是一个万能的类。
很难说多少才算多,没有什么黄金法则。然而,如果你发现自己不断地在不同的类之间切换,以了解正在发生的事情,那么这就表明你有太多的类。同样地,如果你不能理解为什么某个委托是有用的,那就说明它太小了,你把事情分割得太多了。
你还应该注意创建保留循环。大多数情况下,委托属性应该是弱的。如果一个对象一定要设置一个委托,可以考虑将委托作为一个输入添加到对象的初始化器中,并使用"!"将其类型标记为强制解包,而不是通过"!"标记为可选。这将迫使消费者在使用该对象之前设置委托。
如果你发现自己很想创建一个强大的委托,另一种设计模式可能更适合你的使用情况。例如,你可以考虑使用策略模式来代替。更多细节请参见第5章。
游乐场的例子已经让你尝到了实现委托模式的滋味。现在是时候将这一理论用于应用程序中了。你将继续上一章的RabbleWabble应用,并添加一个菜单控制器来选择问题组。
如果你跳过了上一章,或者你想重新开始,请打开Finder并导航到你下载本章资源的地方,然后在Xcode中打开starter/RabbleWabble/RabbleWabble.xcodeproj。
你将创建一个新的视图控制器,让用户从问题组的选项列表中进行选择,而不是仅仅显示基本的短语问题。
在文件层次结构中,右键点击控制器,选择新文件。选择iOS标签,从列表中选择Swift文件,然后点击下一步。输入SelectQuestionGroupViewController.swift作为文件名,点击创建。
将SelectQuestionGroupViewController.swift的内容替换为以下内容。
import UIKit
public class SelectQuestionGroupViewController: UIViewController {
// MARK: - Outlets
@IBOutlet internal var tableView: UITableView! {
didSet {
tableView.tableFooterView = UIView()
}
}
// MARK: - Properties
public let questionGroups = QuestionGroup.allGroups()
private var selectedQuestionGroup: QuestionGroup!
}
你将使用tableView来显示一个问题组的列表。每当设置tableView时,你要把tableView.tableFooterView设置为一个空白的UIView。这个技巧是为了防止表视图绘制不必要的空表视图单元格,它在默认情况下会在所有其他单元格绘制完毕后绘制。
你将questionGroups设置为QuestionGroup.allGroups(),这是一个由QuestionGroupData.swift中定义的扩展所提供的方便方法,简单地返回所有可能的QuestionGroup。
简单地返回所有可能的QuestionGroup选项。
以后你将使用selectedQuestionGroup来保持用户所选择的任何一个QuestionGroup。
接下来,你需要使SelectQuestionGroupViewController符合UITableViewDataSource以显示表格视图单元。在文件的末尾添加以下扩展。
// MARK: - UITableViewDataSource
extension SelectQuestionGroupViewController: UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return questionGroups.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell()
}
}
现在,你只需从tableView(_:, cellForRowAt:)返回一个空的UITableViewCell作为占位符。
为了真正实现这一点,你需要一个自定义的UITableViewCell子类。这将使你能够完全控制单元格的外观和感觉。在文件层次结构中,右键单击视图,选择新文件。
选择iOS标签,从列表中选择Swift文件,然后点击下一步。输入QuestionGroupCell.swift作为文件名,然后点击创建。
把QuestionGroupCell.swift的内容替换成以下内容。
import UIKit
public class QuestionGroupCell: UITableViewCell {
@IBOutlet public var titleLabel: UILabel!
@IBOutlet public var percentageLabel: UILabel!
}
你将很快创建这个视图并连接出口,但现在,再次打开SelectQuestionGroupViewController.swift。
将现有的tableView(_:, cellForRowAt:)替换为以下内容。
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell( withIdentifier: "QuestionGroupCell") as! QuestionGroupCell
let questionGroup = questionGroups[indexPath.row]
cell.titleLabel.text = questionGroup.title
return cell
}
构建并运行,以确保你没有任何编译器警告。然而,你还不应该看到任何不同的东西,因为你还没有真正把SelectQuestionGroupViewController添加到应用程序中。接下来你会做这个。
打开Main.storyboard,选择对象库按钮,在出现的新窗口的搜索栏中输入UIViewController。
按住Option键以防止窗口关闭,然后拖放一个新的视图控制器到现有场景的左边。
接下来,在对象库窗口的搜索栏中输入UITableView,并将一个新的表视图拖放到新的视图控制器上。
选择表视图,然后选择 "添加新约束 "图标,并执行以下操作。
将顶部约束设置为0。
将前导约束设置为0。
将尾部约束设置为0。
将底部约束设为0。
取消对边距的限制。
按添加4个约束条件。
在对象库窗口的搜索栏中输入UITableViewCell,并将一个表视图单元拖放到表视图上。
最后,在对象库窗口的搜索栏中输入标签,并将两个新标签拖到表视图单元上。
然后,按下对象库窗口上的红色X,将其关闭。
双击第一个标签,将其文本设置为 "标题"。把它放在单元格的最左边,与顶部和左边的边距对齐(它应该显示蓝色指示器)。双击第二个标签,将其文本设置为0%。把它放在单元格的最右边,与顶部和右侧的边距对齐。
你的场景现在应该看起来像这样。
你现在需要在标签上设置约束。
选择 "标题 "标签,然后选择 "添加新约束 "图标,并执行以下操作。
将顶部约束设置为0。
将前导约束设置为0。
将尾部约束设为8。
将底部约束设置为0。
确认对边距的约束被选中。
按添加4个约束条件。
选择0%的标签,然后选择 "添加新约束 "图标,并进行以下操作。
将顶部约束设置为0。
将尾部约束设置为0。
将底部约束设置为0。
确认对边距的约束被选中。
按添加3个约束条件。
最后,选择百分比标签,进入尺寸检查器,向下滚动到内容拥抱优先,并将水平设置为750。
很好! 你已经把视图都设置好了。接下来你需要设置类的标识,重复使用标识,并挂上IBOutlets。
选择表视图单元格,转到身份检查器,将类设置为QuestionGroupCell。
仍然选择单元格,切换到属性检查器,将标识符设为QuestionGroupCell。
切换到连接检查器,把titleLabel出口拖到标题标签上,把百分比标签拖到0%标签上。
接下来,选择场景中的黄色视图控制器对象,转到身份检查器,将类设置为SelectQuestionGroupViewController。
仍然选择SelectQuestionGroupViewController,进入Connections Inspector,然后将tableView出口拖放到场景中的表视图上。
接下来,选择场景中的表视图,进入连接检查器,将数据源和委托出口都拖放到黄色视图控制器对象上。
为了在应用程序打开时显示SelectQuestionGroupViewController,你需要将其设置为初始视图控制器。
要做到这一点,将目前指向QuestionViewController场景的箭头拖放至指向SelectQuestionGroupViewController场景。
它应该看起来像这样。
构建并运行,看到问题组显示在表格视图上。真棒!我的天哪
然而,点击一个单元格,应用程序什么也没做。你的下一个工作是把它弄好。
再次打开Main.storyboard,选择SelectQuestionGroupViewController场景。按下编辑器菜单按钮,然后嵌入▸导航控制器。
点击SelectQuestionGroupViewController场景中新添加的导航条,选择导航项,然后进入属性检查器,将标题设置为Select Question Group。
接下来你需要创建一个通往QuestionViewController场景的分隔线。
要做到这一点,选择QuestionGroupCell,然后控制拖拽它到QuestionViewController场景上。
在出现的弹出窗口中选择显示。这就创建了一个转场,每当用户点击表视图单元格就会触发。
建立并运行,尝试点击第一个表格视图单元。真棒;你可以看到问题
按下后退按钮,尝试点击第二个单元格。哦,等等......这些是同样的问题吗?是的,它们确实是。你需要在QuestionViewController上设置选定的QuestionGroup。要做到这一点,你需要让SelectQuestionGroupViewController符合UITableViewDelegate的要求,以便在点击表格视图时被通知。
打开SelectQuestionGroupViewController.swift,在文件的末尾添加以下扩展。
// MARK: - UITableViewDelegate
extension SelectQuestionGroupViewController: UITableViewDelegate {
// 1
public func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
selectedQuestionGroup = questionGroups[indexPath.row]
return indexPath
}
// 2
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
// 3
public override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let viewController = segue.destination as? QuestionViewController else {
return
}
viewController.questionGroup = selectedQuestionGroup
}
}
下面是这些方法中的每一种都在做什么。
selectedQuestionGroup仍然是空的。
在tableView(_:, didSelectRowAt:)中,你只需取消对表视图单元格的选择。这只是一个小技巧,所以如果你以后回到这个视图控制器,你不会看到任何选定的单元格。
在prepare(for:, sender:)中,你要确保segue.destination实际上是一个QuestionViewController(以防万一!),如果是的话,你要把它的
questionGroup到选定的QuestionGroup。
建立并运行,并尝试像之前那样选择第一个和第二个表视图单元格,以验证其工作是否符合预期。
干得好!
这个应用程序已经开始运行了,但仍然缺少一些东西。
在QuestionViewController上实际显示问题组的标题不是很好吗?你肯定会的。
你也无法看到QuestionViewController中还有多少问题。如果能显示出来就好了!
此外,如果你点击QuestionViewController中的所有问题(按绿色复选或红色X按钮),最后什么也没有发生。如果能发生点什么就好了!
最后,按照惯例,"呈现 "控制器在按下 "取消 "按钮时,会通知其调用者,通常是通过一个代理。目前还没有取消的选项,但有一个返回按钮。如果能用一个自定义栏的按钮项来代替它,那就更好了。
虽然这听起来有点费事,但所有这些实际上只是几行代码而已。你可以做到这一点!
为了解决第一个问题,打开QuestionViewController并替换。
public var questionGroup = QuestionGroup.basicPhrases()
与以下内容。
public var questionGroup: QuestionGroup! {
didSet {
navigationItem.title = questionGroup.title
}
}
建立并运行,瞧,标题就显示在导航栏上了!
要解决第二个问题,请在其他属性之后直接添加以下内容。
private lazy var questionIndexItem: UIBarButtonItem = {
let item = UIBarButtonItem(title: "",
style: .plain,
target: nil,
action: nil)
item.tintColor = .black
navigationItem.rightBarButtonItem = item
return item
}()
最后,在showQuestion()的末尾添加以下一行。
questionIndexItem.title = "\(questionIndex + 1)/" + "\(questionGroup.questions.count)"
建立和运行,并尝试点击问题。很酷,对吗?
解决后两个问题有点棘手。你需要为它们创建一个自定义的委托。幸运的是,这也是很容易做到的。
在QuestionViewController.swift的顶部添加以下内容,下面是导入UIKit。
public protocol QuestionViewControllerDelegate: class {
// 1
func questionViewController(
_ viewController: QuestionViewController,
didCancel questionGroup: QuestionGroup,
at questionIndex: Int)
// 2
func questionViewController(
_ viewController: QuestionViewController,
didComplete questionGroup: QuestionGroup)
}
以下是你将如何使用这些方法。
取消按钮时,你将调用questionViewController(_:didCancel:at:),这个按钮你还没有创建。
完成所有的问题。
你还需要一个属性来控制这个委托。在//MARK下面添加以下内容: - 实例属性。
public weak var delegate: QuestionViewControllerDelegate?
接下来,你需要设置这个委托。打开SelectQuestionGroupViewController.swift,在prepare(for:sender:)的末尾添加以下内容。
viewController.delegate = self
然而,这将导致编译器错误,因为你还没有让SelectQuestionGroupViewController符合QuestionViewControllerDelegate。
为了解决这个问题,请在文件的末尾添加以下扩展。
// MARK: - QuestionViewControllerDelegate
extension SelectQuestionGroupViewController: QuestionViewControllerDelegate {
public func questionViewController(
_ viewController: QuestionViewController,
didCancel questionGroup: QuestionGroup,
at questionIndex: Int) {
navigationController?.popToViewController(self, animated: true)
}
public func questionViewController(
_ viewController: QuestionViewController,
didComplete questionGroup: QuestionGroup) {
navigationController?.popToViewController(self, animated: true)
}
}
现在你只需弹出SelectQuestionGroupViewController,不管调用哪个委托方法。
接下来你需要适当地调用这些委托方法。
打开QuestionViewController.swift,将viewDidLoad()替换为以下内容,暂时忽略编译器关于缺少方法的错误。
public override func viewDidLoad() {
super.viewDidLoad()
setupCancelButton()
showQuestion()
}
接下来,在viewDidLoad()下面添加以下两个方法。
private func setupCancelButton() {
let action = #selector(handleCancelPressed(sender:))
let image = UIImage(named: "ic_menu")
navigationItem.leftBarButtonItem =UIBarButtonItem(
image: image,
landscapeImagePhone: nil,
style: .plain,
target: self,
action: action)
}
@objc private func handleCancelPressed(sender: UIBarButtonItem){
delegate?.questionViewController(
self,
didCancel: questionGroup,
at: questionIndex)
}
这设置了一个新的取消按钮作为navigationItem.leftBarButtonItem,当它被按下时调用handleCancelPressed(sender:)来通知代理。
构建并运行,试试你的新取消按钮吧
最后,仍然在QuestionViewController.swift中,向下滚动,用下面的内容替换//TODO: - 处理这个...!注释。
delegate?.questionViewController(self, didComplete: questionGroup)
建立并运行,选择 "基本短语 "单元,因为这只有几个问题。按红色的X或绿色的检查按钮,直到你到达终点,看看应用程序现在是如何弹回SelectQuestionGroupViewController的。很好!
你在本章中了解了委托模式,包括如何使用苹果提供的委托以及如何创建你自己的委托。以下是你学到的关键点。
委托模式有三个部分:一个需要委托的对象、一个委托协议和一个委托。
这种模式允许你将大的类分解,并创建通用的、可重用的组件。
在绝大多数的用例中,委托应该是弱属性。
RabbleWabble开始出现了! 然而,要使它成为下一个App Store的成功,还有很多事情要做。
继续阅读下一章,了解策略设计模式并继续构建RabbleWabble。
上一章 | 目录 | 下一章 |
---|