作为开发人员,我们一直在寻找,学习和尝试不同的软件开发方法。我一直在寻找方法来更好地管理APP的复杂性并编写更具表现力和可维护性的代码。最近,我发现了几篇文章讨论了一种企业设计模式,该模式已开始在iOS开发中应用。这种企业模式称为 应用程序控制器模式(Application Controller pattern)。
我首先在Soroush Khanlou的博客文章 8模式帮助您销毁大量视图控制器 中遇到了这个概念,他提到了一种称为Navigator的设计方法。该设计概念将视图控制器的表示封装到一个单独的对象中。进一步浏览他的博客时,我发现他在NSSpain上发表了一个主题为 “协调器”的演讲 ,他在其中阐明并扩展了原始概念。 在演讲中,他将设计的起源归功于Martin Fowler所著的 《企业应用程序架构的模式》 一书。 该概念正式命名为“应用程序控制器”模式。Soroush并不是唯一讨论这种将导航职责移至视图控制器之外的iOS开发人员,在Krzysztof Zablocki,Alberto Debortoli等人讨论的标题为 Flow Controllers的 文章中也可以找到其他几篇文章。
展望未来,为了简化和与Soroush保持一致,我将这个概念称为流程协调器,或者仅称为协调器。我觉得控制器一词在iOS中已经具有一定的含义,我们希望避免任何不必要的混淆。术语协调器似乎更适合这些对象的角色。
我们都已经看到(并编写了)从现有视图控制器推送或模态显示新视图控制器的代码。通常,我们创建新的视图控制器,传入可能需要的任何数据,然后直接呈现(模态)或获取导航控制器,并告诉它“push”新的视图控制器。
let viewController = CustomViewController()
viewController.data = NSObject()//一些数据对象
navigationController.show(viewController, sender:self)
通常在给定的用户流程中重复此过程,其中每个先前的视图控制器配置后续的视图控制器。这带来了几个问题,例如子对象直接访问父对象,下游视图控制器所需的任何数据都需要通过所有先前的视图控制器传递,使测试变得更加复杂,以及如果一个流程需要在不同的设备类型中使用的话,我们必须添加逻辑检查。协调器模式有助于减轻这些问题,并使视图控制器更易于重用,允许重用用户流,并在对象之间分离关注点。
基本概念确实非常简单,您可以将所有特定于导航的UIKit调用移至自定义对象中。这些对象负责为APP中的每个特定流甚至子流执行导航。例如,如果您的APP中有注册流程,那么注册协调员将处理该流程。忘记密码流程,由“忘记密码”协调器处理。还有一个初始的APP协调器,它将启动应用程序流并根据需要生成新的协调器,以处理新的用户流。协调器能够派生其他协调器,因此可以更轻松地重用工作流程和协调器。给定的协调器负责创建任何必需的视图控制器,视图模型,并通过代理来响应任何操作。
由于最初的介绍未包含代码示例,并且原文对代码也进行一些讲解(GitHub上有一个可以下载和查看的APP),所以我决定自己实施流程协调器,以更好地了解潜在的好处。复习有关该主题的众多帖子只会使问题变得更加复杂,因为不同的作者在其“协调器”中采用了不同的技术。一些人将流协调器注入到流的基本视图控制器中,而其他人则使用协议和代理,还有一些人使用块/闭包。一些作者仅使用协调器进行导航,而另一些作者则建议协调器处理网络或持久性访问(将视图控制器保留为仅显示对象)。因为我想获得基本的了解,所以我只专注于导航。最后,我决定也使用代理方法,因为这更松散地耦合了控制器和协调器,并定义了清晰的界面。但是,它确实需要一些额外的样板代码,因为每个视图控制器都需要声明一个协议以代理回合适的流控制器。
为了更好地了解流程协调器在实际应用中的工作方式,我决定创建一个简单的iOS应用,该应用一旦用户登录便具有注册流程和配置流程。配置流程模拟了标签栏控制器以及其他内容。当然,这是一个人为的示例,但它使我们有机会了解流程协调器如何处理导航以及自定义导航动画。
登录按钮将带您直接进入配置屏幕,我们在此处通过伪选项卡栏模拟多个功能。设置将以模态方式显示视图控制器,而关注者和关注者将使用自定义导航代理转换为相应的视图控制器。“注册”按钮将在两个简单的屏幕之间导航,以模拟用户名,密码选择和帐户生成。为简单起见,实际上不需要任何值,并且实际上没有处理或验证任何内容。
总体上建立流程协调器并不困难。我们首先在AppDelegate中创建AppCoordinator。在这里,我们创建一个根视图控制器,并使用它初始化AppCoordinator,然后调用start。
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var appCoordinator:AppCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// 应用程序启动后进行自定义的替代window。
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UINavigationController()
appCoordinator = AppCoordinator(with: window?.rootViewController as! UINavigationController)
appCoordinator?.start()
window?.makeKeyAndVisible()
return true
}
}
我们的第一位协调器是AppCoordinator。该协调器检查用户的登录状态,并根据结果显示登录屏幕或配置屏幕。两者的屏幕和相关流程均由两个子协调器控制。逻辑非常简单,易于遵循。AppCoordinator充当其子协调器的代理,并在身份验证过程完成时接收响应,以便它可以移至下一个流程(配置流程)。
import UIKit
class Coordinator { }
class AppCoordinator {
fileprivate var isLoggedIn = false
fileprivate let navigationController:UINavigationController
fileprivate var childCoordinators = [Coordinator]()
init(with navigationController:UINavigationController) {
self.navigationController = navigationController
}
deinit {
print("deallocing \(self)")
}
func start() {
if isLoggedIn {
showProfile()
} else {
showAuthentication()
}
}
fileprivate func showProfile() {
let profileFlowCoordinator = ProfileFlowCoordinator(navigationController: navigationController)
profileFlowCoordinator.delegate = self
profileFlowCoordinator.start()
childCoordinators.append(profileFlowCoordinator)
}
fileprivate func showAuthentication() {
let authenticationCoordinator = AuthenticationCoordinator(navigationController: navigationController)
authenticationCoordinator.delegate = self
authenticationCoordinator.start()
childCoordinators.append(authenticationCoordinator)
}
}
extension AppCoordinator : AuthenticationCoordinatorDelegate {
func coordinatorDidAuthenticate(coordinator:AuthenticationCoordinator) {
removeCoordinator(coordinator: coordinator)
showProfile()
}
//we need a better way to find coordinators
fileprivate func removeCoordinator(coordinator:Coordinator) {
var idx:Int?
for (index,value) in childCoordinators.enumerated() {
if value === coordinator {
idx = index
break
}
}
if let index = idx {
childCoordinators.remove(at: index)
}
}
}
extension AppCoordinator : ProfileFlowCoordinatorDelegate {
//TODO:
}
接下来是我们的身份验证协调器,顾名思义,它将对用户进行身份验证,或者在我们的情况下,它将引导我们完成伪注册过程。在此示例中,有两个屏幕依次显示,以模拟选择用户名,密码和生成帐户。身份验证协调器根据“下一步”或“创建帐户”操作处理每个后续屏幕的显示。在此流程中,我们使用标准的push导航动画。
import UIKit
protocol AuthenticationCoordinatorDelegate:class {
func coordinatorDidAuthenticate(coordinator:AuthenticationCoordinator)
}
class AuthenticationCoordinator:Coordinator {
weak var delegate:AuthenticationCoordinatorDelegate?
let navigationController:UINavigationController
let loginViewController:LoginViewController
init(navigationController:UINavigationController) {
self.navigationController = navigationController
self.loginViewController = LoginViewController()
}
deinit {
print("deallocing \(self)")
}
func start() {
showLoginViewController()
}
func showLoginViewController() {
loginViewController.delegate = self
navigationController.show(loginViewController, sender: self)
}
func showSignupViewController(){
let signup = SignupViewController()
signup.delegate = self
navigationController.show(signup, sender: self)
}
func showPasswordViewController(){
let password = PasswordViewController()
password.delegate = self
navigationController.show(password, sender: self)
}
}
extension AuthenticationCoordinator : LoginViewControllerDelegate {
func didSuccessfullyLogin() {
print(navigationController.childViewControllers)
delegate?.coordinatorDidAuthenticate(coordinator: self)
}
func didChooseSignup() {
showSignupViewController()
}
}
extension AuthenticationCoordinator : SignupViewControllerDelegate {
func didCompleteSignup() {
showPasswordViewController()
}
}
extension AuthenticationCoordinator : PasswordViewControllerDelegate {
func didSignupWithEmailAndPassword(email: String, passowrd: String) {
delegate?.coordinatorDidAuthenticate(coordinator: self)
}
}
不需要子协调器属性,因为此流不会派生任何其他流,它只需完成并通知父(AppCoordinator)即可。如果一切按计划进行,那么我们将进入配置协调器。此流程模拟了APP中具有访问其他内容(“关注”和“关注者”以及“设置”页面)的基本启动屏幕。此流程使用自定义导航代理,以便在过渡到“关注/关注者”视图控制器时使用自定义动画,同时将设置显示为标准模态。
import UIKit
protocol ProfileFlowCoordinatorDelegate:class { }
class ProfileFlowCoordinator:Coordinator {
fileprivate let navigationController:UINavigationController
fileprivate let profileViewController:ProfileViewController
fileprivate let navigationDelegate:NavigationControllerDelegate?
fileprivate var isProfileViewContoller:Bool {
guard let _ = navigationController.topViewController?.isKind(of: ProfileViewController.self) else { return false }
return true
}
weak var delegate:ProfileFlowCoordinatorDelegate?
init(navigationController:UINavigationController) {
self.navigationController = navigationController
navigationDelegate = NavigationControllerDelegate()
self.navigationController.delegate = navigationDelegate
let viewModel = ProfileViewModel()
self.profileViewController = ProfileViewController(viewModel:viewModel)
}
deinit {
print("deallocing \(self)")
}
func start() {
profileViewController.delegate = self
guard let topViewController = navigationController.topViewController else {
return navigationController.setViewControllers([profileViewController], animated: false)
}
//simple animation function
profileViewController.view.frame = topViewController.view.frame
UIView.transition(from:topViewController.view , to: profileViewController.view, duration: 0.50, options: .transitionCrossDissolve) {[unowned self] (finished) in
self.navigationController.setViewControllers([self.profileViewController], animated: false)
}
}
fileprivate func showFollowingViewController() {
let viewModel = FollowingViewModel()
let following = FollowingViewController(viewModel:viewModel)
following.delegate = self
if isProfileViewContoller {
navigationController.show(following, sender: self)
}
}
fileprivate func showFollowersViewController() {
let viewModel = FollowersViewModel()
let followers = FollowersViewController(viewModel:viewModel)
followers.delegate = self
if isProfileViewContoller {
navigationController.show(followers, sender: self)
}
}
fileprivate func showSettingsViewController() {
let viewModel = SettingsViewModel()
let settings = SettingsViewController(viewModel:viewModel)
settings.delegate = self
navigationController.showDetailViewController(settings, sender: self)
}
fileprivate func popViewController() {
navigationController.popViewController(animated: true)
}
fileprivate func dismissModal() {
navigationController.dismiss(animated: true, completion: nil)
}
}
extension ProfileFlowCoordinator : ProfileViewControllerDelegate {
func didSelectSettingsAction() {
showSettingsViewController()
}
func didSelectFollowingAction() {
showFollowingViewController()
}
func didSelectFollowersAction() {
showFollowersViewController()
}
}
extension ProfileFlowCoordinator : FollowingDelegate {
//TODO:
}
extension ProfileFlowCoordinator : FollowersDelegate {
//TODO:
}
extension ProfileFlowCoordinator : SettingsDelegate {
func dismissSettingsViewController() {
dismissModal()
}
}
同样,逻辑和实现非常简单明了,易于遵循。每个动作(“设置/关注者” /“关注”)由配置文件协调器处理,以显示合适的视图控制器。定制动画器和导航代理处理所有动画方面。这样可使所有组件保持较小且易于理解。
由于我们使用代理,因此我们将引用类型用于协调器。在Swift中实现代理需要定义一个弱代理变量,而弱修饰符仅可用于类协议。我创建了一个空的基类Coordinator,以简化在类型数组中存储子协调器的过程。我们可以将常见的属性(例如childCoordinators属性和导航代理)移动到production app的super类中。
首先,将导航流移动到单独的对象中会感觉很不直观,但是一旦开始使用流协调器,您就会很快意识到其好处。流被很好地包含并从视图控制器中相互隔离。您可以通过为流替换一个不同的导航代理,或将另一个动画器注入到导航代理中,轻松更改给定流的动画。现在,视图控制器对app的流程一无所知,并且不再负责呈现或关闭内容。减少责任是一件好事,我们使视图控制器更加整洁,更易于重用。
Soroush建议协调器还要处理模型改变,以使视图控制器保持为“仅显示”对象。我喜欢在我的应用程序中使用MVVM和RxSwift。如果我们使用的是MVVM,则可以考虑使view model成为视图控制器的代理,然后将所有通过view model的导航动作转发到协调器上。这将取决于您的实现以及在给定导航之前view model是否需要执行任何操作。在MVVM中,view model负责处理对模型改变的请求并不罕见(如果不直接对模型进行改变)。
流程协调器看起来像是另一个潜在有用的工具,可以缓解iOS应用中常见的海量视图控制器问题。我期待在不久的将来在一个完整的项目中对其进行尝试。