子协调员、向后导航、视图控制器之间传递数据等。
在本文中,我们将探讨人们在iOS应用程序中采用协调器模式时面临的六个常见问题的解决方案:
-
您如何以及何时使用子协调员?
-
你如何处理从导航控制器搬回来?
-
如何在视图控制器之间传递数据?
-
如何使用带协调器的标签栏控制器?
-
你是怎么处理赛格的?
-
如何使用协议或闭包?
在这个过程中,我将向您提供许多实践代码,因为我希望您看到如何解决这些问题的真实例子。
如果你错过了我之前关于协调器模式的教程,你应该从这里开始:如何在iOS应用程序中使用协调器模式。
**更喜欢视频?下面的屏幕直播包含了本教程中的所有内容以及更多内容——订阅我的YouTube频道**了解更多信息。
如何以及何时使用儿童协调员
我们将从儿童协调员开始。正如我在第一篇文章中解释的,如果你有一个更大的应用程序,你可以把功能分成子协调员:一个负责创建账户,一个负责购买产品,等等。
所有这些都向父协调器报告,一旦子协调器完成,父协调器就可以继续流程。最终的结果是,我们可以将复杂的应用程序功能拆分成更小的、离散的块,这些块更容易处理,也更容易重用。
但问题是:你什么时候使用这些东西,以及如何最好地完成?
让我们用一个真正的项目来尝试一下——我们将使用第一个视频结尾的同一个协调员项目。我们已经有了一个购买视图控制器和一个创建帐户视图控制器,但是我们将对其进行修改,以便使用子协调器来处理购买。
首先,创建一个名为BuyCoordinator.swift的新Swift文件,并给它以下代码:
class BuyCoordinator: Coordinator { var childCoordinators = [Coordinator]() var navigationController: UINavigationController init(navigationController: UINavigationController) { self.navigationController = navigationController } func start() { }}
对于其start方法,我们需要将一些现有的代码从我们的主协调器移过来,因为这已经创建并显示了Buy View Controller。
因此,打开MainCoordinator.swift并将buy Subcription()的内容移到上面的start()方法,如下所示:
func start() { let vc = BuyViewController.instantiate() vc.coordinator = self navigationController.pushViewController(vc, animated: true)}
在MainCoordinator.swift中将buy Subcription()方法留空——我们稍后将回到它。
现在不可能将自己分配给Buy View Controller的协调器属性,因为它需要一个主协调器。要解决此问题,请打开BuyViewController.swift并将属性更改为购买协调员:
weak var coordinator: BuyCoordinator?
回到主协调器,我们需要创建子协调器的实例,并告诉它接管控制权。这是通过添加三行新代码到buy Subcription()方法来完成的:
let child = BuyCoordinator(navigationController: navigationController)childCoordinators.append(child)child.start()
这就是创建一个子协调员并让它接管控制权所需要的全部,但一个更有趣的问题是,当我们回来时,我们如何处理删除子协调员。稍后我们将研究更高级的解决方案,但首先让我们看看一个简单的解决方案来让我们开始。
对于更简单的应用程序,您可以将子协调器数组视为堆栈,根据需要推送和弹出东西。虽然这工作得很好,但我更喜欢允许在任何时候添加和删除协调器,这给了我更多的树状结构,而不是堆栈。
为了完成这项工作,我们首先需要在购买协调员和主协调员之间建立沟通,这样孩子就可以告诉父母工作已经完成。
第一步是将父协调员属性添加到Buy协调员:
weak var parentCoordinator: MainCoordinator?
这需要弱,以避免保留周期,因为主协调员已经拥有孩子。
我们需要设置当我们的购买协调员被创建时,所以请将此添加到购买订阅()方法的主协调员:
child.parentCoordinator = self
接下来,我们需要一种方法,当Buy View Controller的工作完成时进行报告。我们没有任何特殊的按钮,甚至没有任何形成一个序列的子视图控制器,所以我们将使用这个视图控制器作为购买过程已经结束的信号。
最简单的方法是通过在Buy View Controller中实现view Di DDis出现(),如下所示:
override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) coordinator?.didFinishBuying()}
我们需要将did Finish Buing()方法添加到Buy协调器中。如何处理这取决于您的应用程序流:如果您的主协调器需要专门响应购买完成——也许是同步用户数据,或者导致一些用户界面刷新——那么您可以实现一个特定的方法来处理该流。
在这种情况下,我们将编写一个通用方法来处理所有不需要任何特殊行为的子协调员。
现在就将此方法添加到Buy协调员:
func didFinishBuying() { parentCoordinator?.childDidFinish(self)}
然后,我们可以将child Di Finish()添加到我们的主协调器中:
func childDidFinish(_ child: Coordinator?) { for (index, coordinator) in childCoordinators.enumerated() { if coordinator === child { childCoordinators.remove(at: index) break } }}
它使用斯威夫特**的三等运算符在我们的**数组中找到子协调器。这只适用于类,现在理论上我们的协调器可以被结构使用。
幸运的是,协调器应该始终是类,因为它们需要在许多地方共享,所以我们可以将我们的协调器协议标记为仅类,以使所有这些代码工作。将Coordinator.swift修改为:
protocol Coordinator: AnyObject {
正如你所看到的,子协调员的诀窍是确保你从你的应用程序中分割出一个独立的块让他们处理。这可以帮助您避免大量的协调员,但也使您的代码更容易理解。
向后航行
我们当前的向后导航到以前的视图控制器的解决方案对于简单的项目来说很好,但是当您在子协调器中显示多个视图控制器时会发生什么呢?那么,view Di Dis出现()将被过早调用,并且您的协调器堆栈将被混淆。
幸运的是,**索罗什·**坎卢已经为此写了一个很好的解决方案,他首先开发了协调器模式。
在Buy View Controller中,请完全注释出view Di Dis出现()方法——我们不再需要它了。在购买协调员中,注释did Finish Buy(),因为我们也不需要它。
相反,我们要做的是让我们的主协调器直接检测与导航控制器的交互。首先,我们需要使其符合UI Navigation Control le rF enate协议。只有当它也成为NS Object的子类时,这才是可能的。
将主协调器的定义修改为:
class MainCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
其次,我们需要要求我们的导航控制器告诉我们何时显示视图控制器,方法是让我们的主协调器成为其委托。
将此添加到start()方法:
navigationController.delegate = self
现在我们可以检测视图控制器何时显示。这意味着实现UI Navigation Control le rF enate的didShow方法,读取我们正在移动的视图控制器,确保我们弹出控制器而不是推动它们,然后最终删除子协调器。
现在将此方法添加到主协调员:
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return } if navigationController.viewControllers.contains(fromViewController) { return } if let buyViewController = fromViewController as? BuyViewController { childDidFinish(buyViewController.coordinator) }}
正如你所看到的,后退按钮很棘手的原因是它不是由我们的协调器触发的。幸运的是,使用UI Navigation Control le rF enate协议可以帮助我们干净地监视这些事件。
在视图控制器之间传递数据
在视图控制器之间传递数据似乎在使用协调员时更难,但实际上这是一个很好的方法来确保我们不会在视图控制器之间创建硬链接。
注意:要继续,首先回到原始的协调员项目,这样你就有了一个干净的石板。
在主故事板中,将分段控件拖出到我们的主视图控制器中。这将让用户选择他们想要购买的产品,所以你可以用你的产品名称来填充它。
我们需要为这个分段控件创建一个出口,所以请切换到助理编辑器并创建一个名为product的出口。
现在,在Buy View Controller中,我们想知道选择了哪个订阅产品,所以在那里添加这个属性:
var selectedProduct = 0
该值应该在创建此视图控制器时提供,因此打开MainCoordinator.swift并修改buy Subcription(),以便它接收一个整数参数,并将其直接分配给我们刚刚创建的选择Product属性:
func buySubscription(to productType: Int) { let vc = BuyViewController.instantiate() vc.selectedProduct = productType vc.coordinator = self navigationController.pushViewController(vc, animated: true)}
最后一步是在调用buy Subcription()时将选定的分段索引传递给协调器ViewController.swift.
将buy Tap ed()修改为:
@IBAction func buyTapped(_ sender: Any) { coordinator?.buySubscription(to: product.selectedSegmentIndex)}
正如您所看到的,这里的关键是记住一个视图控制器不知道其他视图控制器的存在。它向协调器传递数据,然后协调器可以去任何地方——也许它触发网络请求,也许它显示视图控制器,或者它做其他事情。协调器计算出目的地;它决定接收的值应该意味着什么。
协调器标签栏控制器
在你的应用中使用标签栏控制器作为清晰划分应用功能的一种方式是很常见的。幸运的是,协调员与他们合作得非常好——把他们放在一起很容易。
您可以看到,应用程序中的每个选项卡都可以由自己的主协调器有效地管理。例如,在我的**用于学习Swift的打开应用程序**中,底部有五个选项卡,每个选项卡都有自己的协调员:学习协调员、实践协调员、挑战协调员等等。
有很多方法可以编码,我将向你展示我是如何做到的。
首先,我创建一个名为Main Tab Bar Controller的UI Tab Bar Controller的新子类。然后,它具有在其选项卡中使用的每个协调器的属性。
我们只有一个协调员,所以我们只需要一个属性。现在将此添加到Main Tab Bar Controller:
let mainCoordinator = MainCoordinator(navigationController: UINavigationController())
然后在它的view Di dLoad()方法中,我在每个协调员上调用start(),这样他们就可以设置他们的基本视图控制器,然后使用我正在使用的每个协调员的导航控制器,将选项卡栏控制器的视图控制器属性设置为所有选项卡的数组。
同样,我们这里只有一个协调员,所以我们将使用它:
override func viewDidLoad() { main.start() viewControllers = [main.navigationController]}
不要忘记给你的每个视图控制器一个标签栏项目,否则你不会在标签栏中看到太多。
例如,我们可以做到这一点的一种方法是在主协调员的start()方法中–我们可以让主视图控制器有一个收藏夹图标,像这样:
vc.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 0)
现在,只需返回到您的应用代表,删除现有的协调器代码,并创建一个新的Main Tab Bar Controller类实例作为窗口的root View Controller:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = MainTabBarController() window?.makeKeyAndVisible() return true}
因此,处理标签栏控制器的聪明方法是每个标签有一个协调员。它将应用程序的不同部分整齐地分开,这意味着我们应用了我们已经知道的相同技术。
处理segue
通过在视图控制器之间创建链接,将segue添加到您的故事板中,或者由ios自动触发,或者由我们直接调用准备(for segue:)。
问题是segue打败了协调器的一个主要好处:它们迫使我们进入一个特定的应用程序流,阻止我们自由地重新排列视图控制器。
是的,我通常使用故事板来设计我的用户界面,因为它们附带了一些好处,比如在表格视图中设计静态单元格的能力。但我不使用它们来添加segue。
请看Xcode默认主细节应用模板中的示例代码:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetail" { if let indexPath = tableView.indexPathForSelectedRow { let object = objects[indexPath.row] as! NSDate let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController controller.detailItem = object controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem controller.navigationItem.leftItemsSupplementBackButton = true } }}
老实说,这只会伤害我的眼睛——它使用字符串名读取segue的类型,然后强制转换目标视图控制器,然后注入值。这不是我想要的应用程序代码。
简单的事实是:我不使用segue。在故事板中,它们迫使我们进入固定的应用程序流,在代码中,我们必须做大量的类型转换才能使它们有用——它们对我来说不太好用。
使用协议和闭包
当使用协调员时,人们会感到困惑的一件事是他们与代表有什么不同。简单的事实是,他们没有——他们实际上只是一个专门的代表。事实上,如果我们使用一个视图控制器并将其重命名为协调器来委派,那么就是这样了。
使用协调器名称的优点是,很明显这是一个专门的委托,处理我们应用程序的导航。对于一个视图控制器,你可能有几个不同的委托,所有这些委托都在一起工作,使其在我们的应用程序中工作——如果你只是使用委托这个名字,会很混乱。通过将这个东西命名为协调器,我们清楚地表明了它的作用:它是推动我们应用前进的东西。
我们有两种选择,而不仅仅是将我们的协调员重新命名为代表并将其留在那里。我想在这里向你们展示它们,但是请再次回滚到您最初的协调员项目,以避免混淆。
首先,我们可以使用协议。对于较大的应用程序来说,这通常是一个更好的主意,因为它允许我们自由地使用不同的具体实现。
现在,我们有一个主协调器类,它有buy Subcription()和create Date()方法,所以让我们用新协议来包装它们。
首先,创建一个名为Buying.swift的新文件,并给它以下代码:
protocol Buying: AnyObject { func buySubscription()}
现在创建第二个协议,称为帐户创建:
protocol AccountCreating: AnyObject { func createAccount()}
我们可以立即使Main协调器符合这两种方法,因为它实现了这些方法:
class MainCoordinator: Coordinator, Buying, AccountCreating {
现在我们可以更新View Controller类来引用这些协议,而不是具体的类型:
weak var coordinator: (Buying & AccountCreating)?
我们不在乎是什么实现了这两个协议,只要有东西实现了——它可能是一个协调员,也可能是其他什么东西。在需要更大灵活性的大型应用中使用协议非常有用,因为您可以自由添加额外的协议,交换不同的协调员进行A/B测试,等等。
另一种选择是删除具体的协调器类型,并删除协议,而是使用闭包。当视图控制器只触发一个或两个操作时,这可以很好地工作。
要使用这种方法,首先将我们的每个闭包定义为View Controller上的属性,如下所示:
var buyAction: (() -> Void)?var createAccountAction: (() -> Void)?
如果你愿意,你当然可以让这些参数。
我们可以调用buy Tap ed()中的那些,然后创建Account Tap ed(),就像这样:
@IBAction func buyTapped(_ sender: Any) { buyAction?()}@IBAction func createAccountTapped(_ sender: Any) { createAccountAction?()}
这是我们更新的视图控制器,所以现在我们只需要为主协调器中的闭包提供值。
首先删除将我们自己分配给协调器属性的行,并改为设置buy Action和create Account Action:
vc.buyAction = { [weak self] in self?.buySubscription()}vc.createAccountAction = { [weak self] in self?.createAccount()}
就是这样!我们仍然得到相同的结果,但是现在我们的视图控制器不知道协调器正在控制导航。
您现在已经看到了使用协议可以让更大的应用程序受益,因为它允许我们将协调员换成任何我们想要的东西。特别是,协议组合意味着我们描述我们想要的行为,而不是我们想要的特定类型,这要灵活得多。
至于使用闭包,我认为当你只有一两个对协调员的回调时,这是一个很好的解决方案,特别是因为它们意味着你的视图控制器是完全隔离的——他们甚至不知道协调员的存在。不过,它们的伸缩性不太好——如果您发现自己添加了三个或更多的闭包属性,您可能希望切换到协议。
下一步在哪里?
这篇文章回答了人们在他们的应用程序中采用协调员时问我的六个常见问题,我希望你在这一过程中学到了一些有用的技巧。
和往常一样,我想建议你阅读**索罗什·坎卢关于协调员的博客**文章——他在博客中讨论了更多向后移动和处理模型突变的解决方案。
告诉我你是如何使用协调员的——发推特给我@twostraws,或者**订阅我的YouTube频道**。