(翻译) 为ViewController解耦 —— 如何在iOS应用程序中使用协调器模式

(翻译) 为ViewController解耦 —— 如何在iOS应用程序中使用协调器模式_第1张图片
1.jpg



原创作者:Paul Hudson
原文链接:https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps



在iOS项目中使用协调模式可以将关于导航部分的逻辑从 ViewController 中抽离出来,能够帮助我们让代码更易于复用和管理,可以让我们根据需要随时调整应用程序的流程。


这是解决 “臃肿的ViewController” 这个问题系列教程中的第一部分:

  1. 如何在 iOS app 中使用协调模式

  2. 如何把数据源和代理从你的 ViewController 抽离出来

  3. 如何把关于视图构建的代码搬离 ViewController




ViewController 当独立存在于 app 中工作方式最好,它们不需要知道自己在应用程序处于什么位置,甚至不知道它们一开始就是一个流的一部分。这样不仅能够让我们的代码易于测试、合理,而且让 ViewController 任何地方复用起来更加简单。

在这篇文章中,我会为大家提供一个关于 协调模式 亲身实践的例子,它将把导航逻辑从 ViewController 中抽离出来,用一个独立的类来实现。这是我从 Soroush Khanlou 那里学到的一种模式,听过我演讲的人会知道我多重视 Soroush 和他的工作,协调模式只是我从他博客中学到的许多事情之一。

虽然这么说,但在我继续之前我得声明:我应该强调这是我在自己的应用程序中使用的 协调模式,所以如果我搞砸了什么事情,那是我的问题,而不是 Soroush

为什么我们需要做些改变?

让我们从 iOS开发者平时写了几百上千遍的代码开始看起:

if let vc = storyboard?.instantiateViewController(withIdentifier: "SomeVC") {
    navigationController?.pushViewController(vc, animated: true)
}

在这类的代码中,一个 ViewController 必须知道关于创建、配置、展示另外一个 ViewController 的代码。这导致了在项目中出现紧密耦合的情况:你已经写死了从一个 ViewController 到 另外一个 ViewController 的链接,因此,如果你想从不同的地方显示相同的 ViewController,你甚至可能需要复制配置代码。

那么当你需要为 iPad 用户盲人用户 配置不同的行为,或者说对用户进行 A/B test 的时候会发生什么呢?好吧,你仅有的想法就是在你的 ViewController 里面写上更多的配置代码,这样问题变得越来越糟糕。

更糟糕的是,所有这些都涉及一个 child ViewController 在告诉它的 navigation controller 应该怎么做 —— 我们第一个 ViewController 去找它的父控制器并告诉它,如何去呈现第二个 ViewController

为了彻底地解决这个问题,协调模式 让我们对 ViewController 进行解耦,这样的话我们的 ViewController 就不会知道它是从哪个页面过来,而且不知道它会跳去哪个页面 —— 甚至不知道还有存在一个 视图控制器链

作为替代,你的应用程序流是由 协调者 控制的,而且你的 view 通信也是通过 协调者。如果希望用户进行身份验证,就请 协调者 显示一个身份验证对话框,它可以查明这意味着什么并且适当地展示出来。

这样做的结果就是你会发现你可以在任何顺序下使用 ViewController,当需要的时候可以使用它们、复用它们 —— 在你的应用程序中再也不是写死的跳转逻辑。当你需要在5个不同的地方触发用户身份校验已经不再是问题,因为它们可以在 协调者 中调用相同的方法。

对于更庞大的项目来说,你甚至可以创建 子协调者(child coordinators) 或者 副协调者(subcoordinators) —— 这样你就可以分离你应用导航的一部分。举个例子,你可能用一个 子协调者 控制用户账号创建的流程,然后用另外一个 子协调者 控制订阅的流程。

如果你要更灵活一些,将 ViewControllers 和 协调者 之间的通信从具体的类型换成 protocol 会是个好主意。这允许你在任何时候替换整个 协调者,并获得一个不同的程序流 —— 你可以为 iPhone, iPad, Apple TV 等等设备 提供各自的协调者。

所以,如果你正在与 “臃肿的 ViewControllers ” 做斗争,我想你会发现这对简化你的导航会很有帮助。但是抽象的理论讨论多了,让我们用一个实际的项目来测试 协调模式

实际开发中的 协调者

从新建一个项目开始,在Xcode中新建一个项目,我这里把它命名为 “CoordinatorTest”,但你可以根据你喜好随意命名。

我想介绍接下来的三个步骤,以便为你使用 协调模式 建立良好的基础:

  1. 设计两个 protocols: 一个是将会给我们所有 coordinators 使用的,另外一个是能够让我们的 ViewControllers 易于创建。

  2. 创建一个用于控制我们应用程序流的 main coordinator ,然后在app 启动时候 start 它。

  3. present 其他页面。

就像我上面说的,在 ViewControllers 和 协调者(coordinators)通信使用协议是个好主意,但在下面的简单例子中我们可以使用具体的类。

首先,我们需要一个给所有协调者(coordinators)遵守的 Coordinator 协议。尽管你可以用它做很多事情,但我建议你至少需要这么做:

  1. 一个属性来存储任何子协调器。这里我们不需要子协调器,但我仍然会为它们添加一个属性,这样之后就可以用自己的代码扩展它。

  2. 一个用于存储用于呈现 ViewControllersnavigation controller 的属性。即使你没有在顶部显示导航栏,使用导航控制器是显示视图控制器的最简单方法。

  3. 一个 start() 方法,使协调器获得控制权。这允许我们完全创建一个协调器,并且只有在我们准备好时才激活它。

在Xcode中,按 Cmd+N 创建一个新的Swift文件,名为 Coordinator.swift 。给它这个代码来实现上面的要求:

import UIKit

protocol Coordinator {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }

    func start()
}



在我们制定协议时,我通常会添加一个简单的 Storyboarded 协议,让我可以从 Storyboarded 创建视图控制器。

虽然我很喜欢使用 Storyboarded,但我不喜欢在我的项目中分散使用 Storyboarded 代码 —— 将所有这些代码放到一个单独的协议中不仅可以使我的代码更清晰,而且以后可以灵活地改变。

我不记得在哪里第一次看到这种方法,但是它很简单。我们将:

  1. 创建一个名为 Storyboarded 的协议
  2. 为这个协议添加方法:instantiate(), 这个方法将会返回一个调用它的任何类的实例。
  3. instantiate() 添加一个默认实现,该实现查找使用它的 ViewControllers 的类名,然后使用它在 Main.storyboard 中查找一个 storyboard 标识符。



这能够实现取决于两个因素。

第一,当你使用 NSStringFromClass(self) 来查找你所请求的视图控制器的类名时,你会取回你的 appname.yourviewcontroller 。我们需要编写一小段代码来分割中间点上的字符串,然后使用第二部分(“YourViewController”) 作为实际的类名。

第二,无论何时你将 ViewControllers 添加到你的 storyboard,确保你将它的 storyboard identifier 设置为你给它的类名。

创建第二个 Swift 文件命名为 Storyboarded.swift,然后给它以下协议:

import UIKit

protocol Storyboarded {
    static func instantiate() -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self {
        // this pulls out "MyApp.MyViewController"
        let fullName = NSStringFromClass(self)

        // this splits by the dot and uses everything after, giving "MyViewController"
        let className = fullName.components(separatedBy: ".")[1]

        // load our storyboard
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)

        // instantiate a view controller with that identifier, and force cast as the type that was requested
        return storyboard.instantiateViewController(withIdentifier: className) as! Self
    }
}



我们已经有了Xcode为这个默认项目提供的 ViewController。打开ViewController.swift,让它遵守Storyboarded 协议:

class ViewController: UIViewController, Storyboarded {



现在我们有了一个简单创建 ViewController的方式,我们不再需要 Storyboard 来处理。在iOS中,Storyboard 不仅负责包含 ViewControllers 设计,还负责配置基本的app窗口。

我们将允许 Storyboard 存储我们的设计,但阻止它处理我们的应用启动。所以,请打开 Main.storyboard 并选择它包含的视图控制器:

  1. Main.storyboard右边的属性栏取消选中 Initial View Controller
  2. 现在切换到 identity 检查器并给它一个storyboard 标识符 " ViewController " —— 记住,这为了让 Storyboarded 协议能够生效需要和 class name 匹配。

最后一步的设置是取消 storyboard 作为配置基本的应用程序入口:

  1. 在项目导航器的顶部选择您的项目。
  2. 在 Target 下面选择“CoordinatorTest”。
  3. 查看 Main Interface 选项框 —— 它应该是 “Main”
  4. 删除 “ Main”,把 Main Interface 留空白

现在我们所有的基本代码已经完成了。现在你的项目是不能工作的,但是我们将修复它...

创建并且启动我们的 协调者(coordinator)

此时,我们已经创建了一个 coordinator 协议,定义每个协调器需要能够做什么,一个 Storyboarded协议,让它更容易从 Storyboard 创建 ViewController,然后取消 Main.storyboard 作为应用程序的入口的配置。

下一步是创建第一个 coordinator, 它将负责在应用程序启动时控制应用程序。

新建一个名为 MainCoordinator.swift的 Swift 文件,然后在里面加上下面代码:

import UIKit

class MainCoordinator: Coordinator {
    var childCoordinators = [Coordinator]()
    var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let vc = ViewController.instantiate()
        navigationController.pushViewController(vc, animated: false)
    }
}



让我来分解一下这些代码的作用……

  1. 它是一个类而不是一个结构体是因为这个 coordinator 将在很多个 ViewControllers 中共享。
  2. 它有一个空的 childCoordinators 数组来满足协调协议中的要求,但是我们在这里不使用它。
  3. 它还具有 coordinator 所需的 navigationController 属性,以及设置该属性的初始化器。
  4. start() 方法是主要部分:它使用了我们的 instantiate() 方法去创建一个 ViewController 类的实例,然后push到导航控制器上。

注意到 MainCoordinator 不是视图控制器吗? 这意味着我们不需要与 UIViewController 的任何奇怪的地方斗争,也没有像 viewDidLoad()viewWillAppear() 这样被 UIKit 自动调用的方法。

现在我们的应用程序中已经有了一个 coordinator,我们在应用程序启动的时候需要使用它。通常情况下我们的应用程序启动是由 storyboard 管理的,但现在我们禁用它了,所以需要在 AppDelegate.swift 中手动用代码去配置它。

所以,我们打开 AppDelegate.swift,在里面添加属性:

var coordinator: MainCoordinator?



这会存储我们应用的 主协调器(coordinator),所以它不会马上被释放。

接下来我们会对 didFinishLaunchingWithOptions 方法做一些修改,让它配置并且 start 我们的 MainCoordinator,并且给我们的应用程序设置一个 basic window。再提一次,basic window 通常是由 storyboard 完成配置的,但现在这是我们该做的了。

把现有的 didFinishLaunchingWithOptions 方法用下面的代替:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // create the main navigation controller to be used for our app
    let navController = UINavigationController()

    // send that into our coordinator so that it can display view controllers
    coordinator = MainCoordinator(navigationController: navController)

    // tell the coordinator to take over control
    coordinator?.start()

    // create a basic UIWindow and activate it
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.rootViewController = navController
    window?.makeKeyAndVisible()

    return true
}



如果一切按照我们预想的计划进行,你现在应该可以启动你的应用程序然后看到页面显示。

这时你已经花了大约20分钟了,但是并没有太多的时间来展示你的成果。不过,再跟我多呆一会儿—— 这就要改变了!

管理应用程序流

协调器的存在是为了控制程序在你的应用程序里面的页面流程,而我们现在要展示这是如何做到的。

首先,我们需要一些可以显示的视图控制器。因此,按 Cmd+N 创建一个新的类,命名为 BuyViewController,让它继承 UIViewController。现在再创建另一个 UIViewController 子类,这次叫做 CreateAccountViewController

第二,回到 Main.storyboard 并拖出两个新的视图控制器。给其中一个类和它的storyboard标识符改成 “BuyViewController”,另一个是 “CreateAccountViewController”。我建议你做一点 UI 布局来定制每个 ViewController —— 添加一个 UILabel 分别给他们标上 buyCreate Account,这样你就可以知道运行的时候是展示哪个页面。

第三,我们需要向第一个 ViewController 添加两个按钮,这样我们就可以点击它们来显示其他ViewController。所以,让我们为它添加标题为 “Buy” 和 “Create Account” 的两个按钮,然后为它们实现 buytap()createAccount() 的点击方法。

第四,我们所有的 ViewController 都需要与它们的 ** coordinator** 通信。如前所述,对于较大的应用程序,你可能想在这里使用协议,但这是一个相当小的应用程序,因此我们可以直接引用我们的 MainCoordinator 类。

所以,我们在三个 ViewController 里面添加这个属性:

weak var coordinator: MainCoordinator?



当你打开 BuyViewControllerCreateAccountViewController 文件的时候,也它们遵守 Storyboarded 协议,这样我们可以更容易地创建它们。

最后,打开 MainCoordinator.swift 文件,并且把它的 start()修改成如下:

func start() {
    let vc = ViewController.instantiate()
    vc.coordinator = self        
    navigationController.pushViewController(vc, animated: false)
}



这设置了初始视图控制器的coordinator 属性,因此它能够在按钮被点击时发送消息。

此时,我们有几个 ViewController 都由一个 ** coordinator** 管理,但我们仍然没有办法在视图控制器之间跳转。

为了实现这一点,我们需要添加两个新方法到MainCoordinator:

func buySubscription() {
    let vc = BuyViewController.instantiate()
    vc.coordinator = self
    navigationController.pushViewController(vc, animated: true)
}

func createAccount() {
    let vc = CreateAccountViewController.instantiate()
    vc.coordinator = self
    navigationController.pushViewController(vc, animated: true)
}



这些方法与 start() 几乎相同,只是现在我们使用的是BuyViewControllerCreateAccountViewController,而不是原来的 ViewController。如果你需要以某种方式配置那些视图控制器,这就是你要做的。

最后一步 —— 是将一些代码放入 ViewController 类的buytap()createAccount()方法中。

这些方法的所有实际的跳转任务已经存在于我们的 coordinator中完成了,所以 IBActions 变得很简洁:

@IBAction func buyTapped(_ sender: Any) {
    coordinator?.buySubscription()
}

@IBAction func createAccount(_ sender: Any) {
    coordinator?.createAccount()
}



你现在应该能够运行你的应用程序,并且能够在页面进行跳转 —— 所有这些都是由 coordinator 触发。

现在该做什么?

我希望这对你了解 coordinator 有所帮助:

  • 没有 ViewController 知道页面跳转链中的下一步是什么或者如何配置它。
  • 任何视图控制器都可以触发coordinator里面的页面流,而且不需要知道它是如何完成的,也不需要重复代码。
  • 可以添加集中代码来处理 ipad 和其他布局变化,或者进行A/B测试。
  • 但最重要的是,你得到了真正的 ViewController 隔离: 每个视图控制器只对自己负责。

你可能感兴趣的:((翻译) 为ViewController解耦 —— 如何在iOS应用程序中使用协调器模式)