协调器 Redux

文章目录
  1. 1. 三个问题
    1. 1.1. 1. App Delegates 中放的东西太多了
    2. 1.2. 2.太多责任
    3. 1.3. 平缓的 Flow
    4. 1.4. Libraries vs Frameworks
  2. 2. Coordinators
    1. 2.1. 代码示例
    2. 2.2. 为什么 Coordinators 很好用
      1. 2.2.1. 1.每个 ViewController 现在是孤立的
      2. 2.2.2. 2.ViewControllers 现在可以复用
      3. 2.2.3. 3.app中每个任务和子任务都有一种专用的封装方式
      4. 2.2.4. 4.Coordinator 将显示绑定与副作用分开
      5. 2.2.5. 5. Coordinator是完全由你控制的对象

原文:Coordinators Redux

今年年初我写过关于 协调器的文章, 但是自从那以后对于协调器的思考已经成熟了很多,我想通过这几个月所学的东西来来重新介绍这个话题。

这是根据我今年在NSSpain的一次演讲改编而成。此处找到幻灯片。在这里找到视频。

三个问题

1. App Delegates 中放的东西太多了

苹果公司在指导我们将代码放在合适的地方方面做得非常糟糕。弄清如何构建 App 完全由我们自己决定。最开始写代码的地方显而易见是 app’s delegate.

app delegate 是每个 App 的入口。它的主要职责是从操作系统到应用程序的子系统来回传递消息。不幸的是,由于它位于所有事物的中心,因此很容易在东西放在这。这样设计方式中的一个受害者就是 rootViewController 的配置。如果你的使用 tabbarController 作为 root,那么你必须要在某个配置所有的 tabbarController 的 children,并且 App delegate 就是个很好的场所。

我写的第一个 App(对于大多数读者我猜测,这是真的),我在我的AppDelegate中,为 rootviewcontroller 配置了所有配置。那些代码真的不属于这里,写在这里只是为了方便。

在我写完我第一个 app 以后我意识到了这个,然后我变得聪明了,我是用了这样的技巧:

1
@interface SKTabBarController : UITabBarController

我会创建一个我想要使用的 rootViewController 子类,然后我会把代码挪到这里。这只是关于这个问题的临时补丁,最后发现这也不是此代码该放的地方。我建议我们研究这个对象——rootViewController,从责任的角度。管理子视图控制器属于这些职责,但分配和配置它们的职责不多。我们正在对一个从未打算进行子类化的东西进行子类化,只是为了我们可以隐藏一些无家可归的代码。

对于此应用程序配置逻辑,我们需要一个更好的家。

2.太多责任

这里有另外一个问题。单个ViewController也遇到这样的问题,把大量的责任放倾倒App delegate里面。

ViewController 中负责的一些事情:

  1. 模型视图绑定
  2. 子视图分配内存
  3. 获取数据
  4. 布局
  5. 数据转换
  6. 导航流程
  7. 用户输入
  8. 模型变化
  9. 还有更多

我提过一些方法将这些责任藏到 ViewController 的 children中,方法在8 Patterns to Help You Destroy Massive View Controller中. 所有责任不能在一个地方,这样 ViewController 中的代码就会少于 3000行了。

哪些东西应该在这个类?哪些应该在别的地方?ViewController的工作是什么?这些问题还没有理清楚。

引用 Graham Lee 说的一句话,我很喜欢。

When you get overly attached to MVC, then you look at every class you create and ask the question “is this a model, a view, or a controller?”. Because this question makes no sense, the answer doesn’t either: anything that isn’t evidently data or evidently graphics gets put into the amorphous “controller” collection, which eventually sucks your entire codebase into its innards like a black hole collapsing under its own weight.

什么是ViewController?Smalltalkian意义上的控制器最初严格是为用户输入而设计的。甚至“控制”一词也给我们带来了麻烦。如我之前所写

When you call something a Controller, it absolves you of the need to separate your concerns. Nothing is out of scope, since its purpose is to control things. Your code quickly devolves into a procedure, reaching deep into other objects to query their state and manipulate them from afar. Boundless, it begins absorbing responsibilities.

平缓的 Flow

最后一个我想要讨论的问题是:navigation flow

1
2
3
4
5
- (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];
}

这事一段很常见的代码,不幸的是,他是垃圾代码。让我们一行行看:

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

第一行的 dataSource 是 ViewController 逻辑上的 child,然后我们正在跟他要我们需要引用的的对象。

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

这里事情开始变得麻烦。ViewController 正在实例化一个新的 ViewController,然后配置它。这个 ViewController ”知道“ 在 flow 中接下来会流出什么。他知道新的 ViewController 是怎么配置的。正在执行 Presenting 的 ViewController 知道他所在 App 位置中的大量细节(新 ViewController的配置细节)。

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

第三行是它完全偏离轨道的地方。这个 ViewController 知道了他的父级 ViewController,因为请记住,这些视图控制器在同一层次结构中,子 ViewController 会向父 ViewController 发送消息,告诉他要做什么。子 ViewController 正在指挥他的父亲。在现实世界里,孩子不应该到处指挥自己的父亲。在编程中,我会说孩子甚至不应该知道他们的父亲是谁!

8种模式帮助你干掉大量的ViewController, 我建议使用一个 Navigator 类型,它可以注入到那些包含导航逻辑的 ViewControllers 中。如果你想要把导航逻辑放到同一个地方, Navigators 是一个不错的解决方案,但是我们很快遇到一个导航器没办法帮我们解决的问题。

在这三行代码中有很多逻辑,但是 ViewController 不是这些逻辑发生的地方。想象你有个编辑图像的 app

你的 PhotoSelectionViewController presents StraighteningViewController presents FilteringViewController presents CaptioningViewController。你的导航 flow 现在分布在三个不同的对象之间。进一步说,某个 ViewController presenting PhotoSelectionViewController ,但是 dismissal 逻辑必须要在 CaptioningViewController 中处理。

传递 Navigator 使这些 ViewControllers 保持链状连接在一起,并不能真正解决每个 ViewController 知道链中下一个的问题。

我们也需要解决这个问题。

Libraries vs Frameworks

我认为Apple 希望我们以所有这些方式编写代码。他们希望我们使用 ViewController 成为世界中心,因为所有以相同样式编写的 app 都可以通过更改SDK发挥最大的影响力。不幸的是,对于开发者,这并不总是最好的举动。我们是负责将来维护 app 的人,可靠的设计和代码可扩展性是我们最看重的。

他们说libraries 和 frameworks 的区别是,你调用 libraries,frameworks 调用你。我想尽可能像依赖 libraries 的方式,对待 3rd-party 依赖。

当使用 UIKit ,你不需要负责什么。调用 -pushViewController:animated: 然后它做一些工作。并且在将来的某个不确定的时间,下一个ViewController出现他会调用 viewDidLoad: 在这里面你可做更多事。你不应该让 UIKit 决定何时运行代码,而是应尽快退出 UIKit -land, 以便你可以完全控制代码的流向。

我曾经将ViewController为 app 中最高等级的东西,这些东西知道如何运行整个app。但是我开始想颠覆这个想法后他会是什么样子。view 对其 ViewController是透明的。view 由 ViewController 控制。如果我们以相同的方式使视图控制器只是另一透明的东西怎么办?

Coordinators

什么是 Coordinators ?

Coordinator 是用于管理一个or 多个ViewController的对象。将所有驱动逻辑从 ViewController中移除,并将这些内容向上移动一层,这会使你的生活变得更加美好。

这一切都从 app Coordinator 开始。Coordinator解决 app delegate 中内容过多的问题。app delegate 可以保留app Coordinator并启动它。app Coordinator 将为app 设置主视图控制器。可以在文献中找到这种模式,例如企业应用程序体系结构模式之类的书。他们把 Coordinator 叫做 Application Controller. app coordinator 是 Application Controller 的特别版,尤其对于 iOS。app coordinator 可以创建并配置 ViewController,或者它可以长生新的子 Coordinator来执行子任务。

coordinators 可以接管 ViewController的哪些任务?主要是 navigation 和 model 变化.(我的意思是通过模型变化将用户的更改保存在数据库中,或者对 API 进行 PUT or POST 请求,这些都会破坏性地修改用户的数据。)

当你把这些任务从ViewController中拿走,我们最终得到了一个惰性的 ViewController。它可以呈现,可以获取数据,对其进行转换以进行呈现、显示,但至关重要的是无法对其进行更改。现在我们知道,每当展示 ViewController 时,都不会让他自己控制。每当需要让我们知道事件或用户输入时,它都会使用委托方法。让我们看一个代码示例。

代码示例

让我们从 App delegate开始

1
2
3
4
5
6
7
- (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];
}

app delegate 构建 app 的window 和 root ViewController,然后开启 app Coordinator。Coordinator的初始化与开始工作是分开的。这样我们就可以按自己的意愿(懒惰,贪婪等)创建它,并且只有在准备好后才能启动它。

Coordinator 就是一个 NSObject:

1
@interface SKAppCoordinator : NSObject

这很棒,这里没有秘密,UIViewController 有上千行代码,我们不知道当我们调用他的方法时会发生什么,因为他是闭源的。简单的 NSObject 类型对象运行app,可使一切变得更加简单。

app coordinator 使用他需要的数据初始化,这些数据包括 root ViewController

1
2
3
4
5
6
- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {  
self = [super init];
if (!self) return nil;
_navigationController = navigationController;
return self;
}

一旦我们调用 -start 方法,coordinator就会开始工作

1
2
3
4
5
6
7
- (void)start {  
if ([self isLoggedIn]) {
[self showContent];
} else {
[self showAuthentication];
}
}

从一开始 Coordinator 就做决定!以前像这样的逻辑没有固定位置,你可以把他放在 appdelegate中或者 ViewController中,但是放这两个地方都有各自的缺陷。在 ViewController中,你有一个 ViewController做超出它本身责任的事情。在 appdelegate中,你会放入一些与他不相关的代码污染他。

让我们研究一下 -showAuthentication 方法。在这里,base Coordinator 生成 子 Coordinator,并让其执行子任务

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

我们使用 childCoordinators 数组引用 Coordinators,防止其被销毁。

ViewController存在一棵树中,并且每个 ViewController都包含一个 view。 view存在于 subViews 树中,而且每个 subview 有一个 layer。layers也存在于一棵树中。因为 childCoordinators ,你也会得到一棵 Coordinators 树。

子 Coordinator 会创建一些 ViewControllers,等待他们工作,viewcontroller 工作完后 Coordinator会通知我们。当 Coordinator 发送信号告知ViewController工作完时,它会清理自己,弹出他添加的所有 ViewController,然后使用委托将消息发送到父级。

一旦我们已认证,我们会得到一个 delegate 消息,然后我们允许子 Coordinator 销毁,然后我们返回平常的程序中。

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

在身份验证 Coordinator 内部,它创建所需的任何Viewcontroller,并将其推入导航控制器。让我们看一下。

1
2
3
4
5
6
7
8
9
10
@implementation AuthCoordinator

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

_navigationController = navigationController;

return self;
}

初始化类似于app coordinator.

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

身份验证需要从“首次运行 viewcontroller”开始。该viewcontroller具有用于注册和登录的按钮,还可能包含一些幻灯片,解释了app。让我们继续使用该Viewcontroller 并成为其代表。

该ViewController 有一个 delegate,因此当用户点击“注册”按钮时,我们可以得到通知。Coordinator 将处理该操作,而不是ViewController需要知道要创建和呈现的注册ViewController。

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

Coordinator 成为这个注册过的 ViewController的代理,为了它可以在按下按钮时通知我们。

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

诸如此类。在这里,我们实际上执行了注册API请求并保存了身份验证 token,然后通知了父coordinator。

每当ViewController发生任何事情(例如用户输入)时,ViewController都会告诉其delegate(在这种情况下为Coordinator),并且Coordinator将执行用户想要的实际任务。让Coordinator来完成这项工作很重要,以便 ViewController 保持惰性。

为什么 Coordinators 很好用

1.每个 ViewController 现在是孤立的

除了知道如何呈现数据,ViewController 啥都不知道。每当有什么事发生时,它都会通知 delegate,但是当然它不知道其 delegate 是谁。

以前,当分支时,ViewController 需要问“好吧,我在iPad还是iPhone上?”。“用户是否正在接受A / B测试?” 他们不再需要提这样的问题。我们是否只是将这个有条件的问题推给了 Coordinator ?从某种意义上讲,但是我们可以以更好的方式解决它。

当确实需要一次具有两个 flow 时,对于A / B测试或多个 size classes,你可以交换整个 Coordinator 对象,而不必在整个 ViewController 上粘贴一堆条件。

如果你想了解 flow 的工作方式,那么这非常容易,因为所有代码都在一个地方。

2.ViewControllers 现在可以复用

ViewControllers 对要显示的上下文或按钮的用途不承担任何责任。它们可以被使用和重新使用,以保持其美观,而不会拖累任何逻辑。

如果你要编写iPad版本的app,则只需替换 Coordinator 即可,并且可以重复使用所有ViewControllers。

3.app中每个任务和子任务都有一种专用的封装方式

即使任务可以在多个 ViewControllers 上运行,它也会被封装。如果你的iPad版本重复使用了其中的一些子任务,但没有重复使用,则仅使用这些子任务就非常容易。

4.Coordinator 将显示绑定与副作用分开

你再也不必担心在呈现 ViewController时,ViewController破坏数据了。它只能读取和显示,不能写入或破坏数据。这与命令查询分离相似。

5. Coordinator是完全由你控制的对象

你不必等待 -viewDidLoad 调用了,才可以进行工作。现在完全可以自己控制工作。在UIViewController超类中没有看不见的代码在做你不知道的事。取代被调用,你开始这个调用。

翻转此模型可以更轻松地了解发生了什么。app的行为对你完全透明,UIKit 现在只是你要使用它时调用的库。

Backchannel SDK 使用此模式来管理其所有 ViewControllers。app coordinator 和身份验证 coordinator 示例来自该项目。

最后,Coordinator 只是一种组织模式。没有你能使用的 Coordinator 库,因为它很简单。没有可以 pod 的 Coordinator库,也米有可以继承的子类。甚至没有一个可遵循的协议。这不是缺点,而是使用像Coordinator这样的模式的优点:它只是你的代码,没有依赖项。

他们将使你的app和代码更易于管理。ViewController将具有更高的可重用性,并且比以往任何时候都更容易开发你的 app。