Coordinator 系列之 认识 Coordinator

2,733 阅读14分钟

Coordinator 模式在Coordinator模式的起源中已经简单介绍过。这篇文章将从三个方面带你更深入认识一下Coordinator 。

需要解决的三大问题

臃肿的 AppDelegate

苹果在引导我们把代码写在正确的地方这件事上做了个不好的示范,如何组织我们的应用代码真的是取决于我们自己。第一个明显的地方是应用程序的委托AppDelegate

AppDelegate是任何 app 的入口。它的主要职责是将消息从操作系统来回传送到应用程序。不幸的是,它出于中心位置,很容易把多余的代码写在那里。这种策略的坏处之一是根控制器的配置。如果你有一个UITabBarController作为根控制器,你必须在某处设置他的所有子控制器,AppDelegate是一个很好的地方。

在我创建的第一个 app 中,我把设置根控制器的所有代码写在了AppDelegate。这些代码真的不属于那里,只是为了方便。

之后我意识到这点,学聪明了,这么写:

@interface SKTabBarController : UITabBarController

我创建了一个子类,把代码都写在了这里。这只是解决问题的一个临时办法,但最终也不是适合这段代码的地方。我建议我们从责任的角度来研究这个对象(根视图控制器)。管理子视图控制器在它的职责范围内,但是创建和配置它们并不在里面。我们正在子类化一个从未打算子类化的东西,只是为了隐藏一些没有归属的代码。

我们需要一个更好的地方来放置应用程序的配置逻辑。

过多的责任

这是另一个令人困惑的问题。就像很容易把大量的责任推给AppDelegate一样,每个单独的视图控制器也会遭殃。

我们让视图控制器做的事情包括但不限于这些

  • 模型-视图绑定
  • 创建子视图
  • 数据获取
  • 布局
  • 数据转换
  • 导航
  • 用户输入
  • 变更模型

这篇文章中,我想出了一些方法,将这些责任隐藏在每个视图控制器的子控制器中。所有这些责任不可能集中在一个地方。这就是我们为何得到3000行视图控制器的原因。

这些东西中哪些应该属于这个类?哪些应该在其他地方?视图控制器的任务是什么?目前还不清楚。

格雷厄姆·李有一句名言我很喜欢。

当你过度依赖MVC时,你会查看你创建的每个类,然后问“这是模型、视图还是控制器?”因为这个问题没有意义,所以答案也没有意义:任何不是明显的数据或图形的东西都会被放入无定形的“控制器”集合中,最终将整个代码库吸进它的内部,就像黑洞在自身的重量下坍塌一样。

视图控制器到底是什么?Smalltalkian意义上的控制器最初是严格为用户输入而设计的。甚至“控制”这个词也会让我们抓狂。正如我以前写过的:

当您将某个东西称为控制器时,它就免除了您分离关注点的需要。没有什么是超出范围的,因为它的目的是控制事物。您的代码很快就会转化为一个过程,深入到其他对象中查询它们的状态并从远处操作它们。无限的,它开始吸收责任。

哪些职责应该在“控制器级别”?格雷厄姆说得对,这个问题根本说不通。因为我们被这个糟糕的词困住了,我们必须非常小心我们允许视图控制器做什么。如果我们不知道,它就是黑洞中心。

导航流

下一个我想讨论的是导航。

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {  
	id object = [self.dataSource objectAtIndexPath:indexPath];  
	SKDetailViewController *detailViewController = [[SKDetailViewController alloc] initWithDetailObject:object];  
	[self.navigationController pushViewController:detailViewController animated:YES];  
}  

这是非常常见的代码,我相信这在苹果的示例中也是一样。不幸的是,它是垃圾。我们来一行行分析。

id object = [self.dataSource objectAtIndexPath:indexPath]; 

第一行没有问题。dataSource是视图控制器的逻辑子元素,我们请求它来获取我们需要引用的对象。

SKDetailViewController *detailViewController = [[SKDetailViewController alloc] initWithDetailObject:object];  

这就是事情开始变得有点棘手的地方。视图控制器正在实例化一个新的视图控制器,即链中的下一个,并对其进行配置。视图控制器“知道”流中接下来会发生什么。它知道如何配置它。正在显示的视图控制器知道很多关于它在 app 世界中位置的细节。

[self.navigationController pushViewController:detailViewController animated:YES]; 

第三行代码完全偏离轨道了。视图控制器现在抓取它的父控制器,因为记住,这些视图控制器存在于一个层次结构中,然后它向父控制器发送一个关于做什么的精确消息。它对它的父母呼来喝去。在现实生活中,孩子们不应该对父母颐指气使。在编程方面,我认为孩子们甚至不应该知道他们的父母是谁!

这篇文章中,我提出了一个Navigator 它可以被注入到包含导航逻辑的视图控制器中。Navigator是一个很好的解决方案。但是我们很快将遇到一个问题,Navigator帮不了我们。

这三行有很多职责,但一个视图控制器不是唯一发生这种情况的地方。假设您有一个照片编辑应用程序。

你的PhotoSelectionViewController弹出了StraighteningViewController,而StraighteningViewController又弹出了FilteringViewControllerFilteringViewController又弹出了CaptioningViewController,你的导航逻辑分散到三个不同的对象中。而且,有个别的类弹出了PhotoSelectionViewController,可是它的取消却在CaptioningViewController

传递一个Navigator使这些视图控制器在一个链中耦合在一起,并不能真正解决每个视图控制器知道链中的下一个的问题。

我们同样需要解决这个问题。

Libraries vs Frameworks

我认为苹果希望我们用这些方式编写代码。他们想让我们以视图控制器为应用程序世界的中心,因为所有的应用程序都以相同的风格编写,这让他们的SDK变化产生了最大的影响。不幸的是,对于开发者来说,这并不是最好的选择。我们是负责维护我们的应用程序到未来的人,可靠的设计和可塑的代码对我们来说是更高的优先级。

他们说库和框架的区别是你调用库,框架调用你。我希望尽可能像对待库一样对待第三方依赖关系。

当使用UIKit时,你没法主导。你调用-pushViewController:animated:,它做了一大堆工作,在未来的某一时刻调用下一个控制器的-viewDidLoad:,在那里你可以做一些额外的工作。与其让UIKit决定你的代码什么时候运行,你应该尽快离开UIKit的地盘,这样你就可以完全控制你的代码如何流动。

我过去认为视图控制器是 app 中最高级的东西,它知道如何运行整个应用。但我开始想如果把它反过来会是什么样子。视图对其视图控制器是透明的。它被它的视图控制器控制。如果我们让视图控制器以同样的方式变得透明呢?

Coordinators

Coordinator 是什么?

协调器是一个控制视图控制器的对象。把所有的驱动逻辑从你的视图控制器中拿出来,把这个逻辑再向上抽一层会让你的生活更棒。

一切都从应用程序协调器AppCoordinator开始。AppCoordinator解决了AppDelegate过重的问题,AppDelegate持有AppCoordinator并启动它。AppCoordinator将会设置app的起始控制器。AppCoordinator可以创建和配置视图控制器,也可以生成新的子协调器来执行子任务。

协调器从视图控制器接管哪些职责?主要是导航和模型变更。我所说的模型变更是指将用户的更改保存到数据库中,或者PUTPOST请求,任何破坏性修改用户数据的行为。

当我们从视图控制器中取出这些任务时,我们得到的是一个惰性的视图控制器。它可以被呈现,它可以获取数据,将数据转换为呈现,显示数据,但最重要的是它不能改变数据。我们现在知道,任何时候我们呈现一个视图控制器,它都不会自己处理东西。每当需要让我们了解某个事件或用户输入时,它都会使用委托方法。让我们来看一个代码示例。

Code Example

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
	self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];  
	self.rootViewController = [[UINavigationController alloc] init];
	
	self.appCoordinator = [[SKAppCoordinator alloc] initWithNavigationController:self.rootViewController];  
	[self.appCoordinator start];
	
	[self.window makeKeyAndVisible];  
}

AppDelegate设置应用程序窗口和根视图控制器,然后启动应用程序协调器。协调器的初始化与启动它的工作是分开的。这让我们可以以我们想要的任何方式创建它(比如懒加载),并且只在我们准备好时才开始。

协调器是一个简单的NSObject对象:

@interface SKAppCoordinator : NSObject  

这非常棒,没有任何秘密。UIViewController有几千行代码,当我们在调它的方法时根本不知道它到底发生了什么,因为它是闭源的。使得运行app的对象为简单的NSObject对象会使一切都变得简单。

应用程序协调器SKAppCoordinator用它需要的数据初始化,这里包括了根视图控制器。

- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {  
	self = [super init];  
	if (!self) return nil;
	
	_navigationController = navigationController;
	
	return self;  
} 

一旦我们触发-start方法,协调员开始工作。

- (void)start {  
	if ([self isLoggedIn]) {  
		[self showContent];  
	} else {  
		[self showAuthentication];  
	}  
} 

从一开始,协调员就在做决定。以前,这样的逻辑是没有正确地方来放置的。你可以把它放到视图控制器或应用委托中,但它们都有缺点。要么你获得一个远远超出它的职责的视图控制器,要么你用无关的东西污染了应用委托。

让我们检查-showAuthentication方法。在这里,我们的父协调器派生出子协调器来执行子任务。

- (void)showAuthentication {  
	SKAuthenticationCoordinator *authCoordinator = [[SKKAuthenticationCoordinator alloc] initWithNavigationViewController:self.navigationController];  
	authCoordinator.delegate = self;  
	[authCoordinator start];  
	[self.childCoordinators addObject:authCoordinator];  
}

我们使用一个childCoordinators 数组来避免子协调器被释放。

视图控制器存在于一个树中,每个视图控制器都有一个视图。视图存在于子视图树中,每个子视图都有一个层。层存在于树中。因为有了这个childCoordinators数组,就得到了一个协调员树。

这个子协调器将创建一些视图控制器,等待它们工作,并在工作完成时通知我们。当协调器发出完成的信号时,它会清理自己,弹出添加的任何视图控制器,然后使用委托将消息返回给其父控制器。

一旦我们进行了身份验证,我们将得到一个委托消息,并且,我们可以允许释放子协调器,然后我们可以回到我们的常规编程。

- (void)coordinatorDidAuthenticate:(SKAuthenticationCoordinator *)coordinator {  
	[self.childCoordinators removeObject:coordinator];  
	[self showContent];  
}

AuthCoordinator中,它创建了它需要的视图控制器,并把它们推入导航控制器,让我们来看看是什么样的。

@implementation AuthCoordinator

- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {  
	self = [super init];  
	if (!self) return nil;
	
	_navigationController = navigationController;
	
	return self;  
}

初始化类似于应用程序协调器SKAppCoordinator

- (void)start {  
	SKFirstRunViewController *firstRunViewcontroller = [SKFirstRunViewController new];  
	firstRunViewcontroller.delegate = self;  
	[self.navigationController pushViewController:firstRunViewcontroller animated:NO];  
}  

授权需要以SKFirstRunViewController开始,这个视图控制器有注册和登录按钮,也许还有引导页。让我们把它推入栈,并让AuthCoordinator成为它的委托。

当用户点击注册按钮时该控制器有一个委托方法来通知到我们。视图控制器不需要知道创建和呈现哪个注册视图控制器,协调器将处理它。

- (void)firstRunViewControllerDidTapSignup:(SKFirstRunViewController *)firstRunViewController {  
	SKSignUpViewController *signUpViewController = [[SKSignUpViewController alloc] init];  
	signupViewController.delegate = self;  
	[self.navigationController pushViewController:signupViewController animated:YES];  
}

让协调器成为注册视图控制器的委托来让它在按钮点击的时候被通知到。

- (void)signUpViewController:(SKSignUpViewController *)signupViewController didTapSignupWithEmail:(NSString *)email password:(NSString *)password {  
	//...  
} 

在这里,我们实际执行注册API请求和保存身份验证令牌的工作,然后通知父协调器。

每当视图控制器发生任何事情时(如用户输入),视图控制器将告诉它的委托(在本例中是协调器),协调器将执行用户想要的实际任务。让协调器做这些工作是很重要的,这样视图控制器就会保持惰性。

Coordinators 的优点

  1. 每个视图控制器现在都是独立的

    视图控制器只知道如何显示数据。无论发生了什么用户事件,它都会告诉它的委托,当然它也不知道它的委托是谁。

    之前,视图控制器需要问“我是在iPad上还是在iPhone上? ”、“正在对用户进行A/B测试吗?”,现在他们再也不用问这样的问题了。我们只是把这个问题,这个有条件的问题,推给了协调者吗?在某种程度上,我们可以用更好的方法来解决。

    当我们需要同时有两个流时,对于A/B测试或多个尺寸的设备,你可以交换整个协调器对象而不是在你的视图控制器上粘贴一堆条件。

    如果您想了解流的工作方式,那么现在就非常简单了,因为所有的代码都在一个地方。

  2. 可重用的视图控制器

    他们不会假设他们将被呈现在什么上下文中,或者他们的按钮将被用来做什么。它们可以被使用和重用,以保持良好的外观,而不需要附带任何逻辑。

    如果你在写你的 iPad 版本的应用,你唯一需要替换的是你的协调器,你可以重用所有的视图控制器。

  3. 应用程序中的每个任务和子任务现在都有专门的封装方式。

    即使任务跨多个视图控制器工作,它也是封装的。如果你的 iPad 版本重用了其中一些子任务,而不是其他的,那么只使用这些子任务真的很容易。

  4. 协调器将显示绑定逻辑分离出来

    你再也不用担心视图控制器在展示时是否会弄乱你的数据。它只能读取和显示数据,不能写入或损坏数据。这是一个类似于命令-查询分离的概念。

  5. 协调器是由你完全掌控的对象

    你不是坐在那里等着-viewDidLoad被调用,你完全控制着app的展示。在UIViewController超类中没有看不见的代码在做您不了解的事情。你主动执行调用,而不是被调用。

    协调器模式可以让你更轻松地了解发生了什么。应用程序的行为对您来说是完全透明的,UIKit现在只是您要使用它时调用的库。

Backchannel SDK 使用这个模式来管理它的所有视图控制器。SKAppCoordinatorAuthCoordinator示例来自该项目。

最终,协调器只是一种组织模式。没有用于协调器的库,因为它们非常简单。没有可以安装的pod,也没有可以派生的子类。甚至没有一个协议需要遵守。这并不是缺点,而是使用像 Coordinator 这样的模式的优点:它只是您的代码,没有依赖项。

这里译者不是很同意。Coordinator 确实只是一种设计模式(或者说是一种特定的委托模式,负责应用程序业务展示的切换流程),同时也不复杂,但它确实有一些一致的行为,我们需要把它抽象成接口或协议来规范这种模式的实现。MVVM 配合上 Coordinator 可以说是非常香。

Coordinator 模式将有助于您的应用程序和代码变得更易于管理,视图控制器将更加可重用,你的应用程序将比以前更优秀。