Coordinators Part II

在本教程的第一部分中,我大体上讨论了coordinator的方法,并展示了一些与我的实现相关的常见示例。为了与本文保持一致,请阅读上一部分。

在这一部分中,我想介绍一些使用协调器的边缘情况。

我想最有趣的部分是:

  • AppDelegate配置
  • 管理启动选项
  • 深度链接和协调员处理推送通知

我将展示一步一步的解决方案并给出解释。

How to configure AppDelegate with main app coordinator?

AppDelegate变得非常简单,因为所有导航逻辑都移动到AppCoordinator。

所以,您需要做的是创建一个协调器,注入rootController并调用start()。

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  
  var window: UIWindow?
  var rootController: UINavigationController {
    return self.window!.rootViewController as! UINavigationController
  }
  private lazy var applicationCoordinator: Coordinator = self.makeCoordinator()
  
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    applicationCoordinator.start()
    return true
  }
  
  private func makeCoordinator() -> Coordinator {
      return ApplicationCoordinator(
        router: RouterImp(rootController: rootController),
        coordinatorFactory: CoordinatorFactoryImp()
      )
  }
}

我们仍然需要在AppDelegate中实现诸如didReceiveRemoteNotification或continueUserActivity之类的委托方法,但是在方法体中,我们只需再次调用协调器start()(我们将通过此方法发送一些数据,我将在本教程的后面部分展示)。作为一个好处,我们可以通过单元测试覆盖协调器的逻辑并保持AppDelegate的清晰。

Let’s introduce Instructor

实际生产应用程序的大多数部分都有不同的启动场景。

avatar

我们希望通过演示教程、显示身份验证流、通知用户新功能,或者简单地显示主屏幕来向用户介绍我们的应用程序。我们应该小心处理这种行为。

假设我们有3种不同的情况:

  • 用户应该看到新手教程(我们显示新手界面)
  • 用户应该已经登录(我们显示登录屏幕)
  • 用户已经完成了前面的两个步骤,只看到主屏幕

为了方便地处理所有这些场景,我们创建了一个enum作为协调器的助手。此枚举应检查所有标志并返回协调器应遵循的正确方案。我将这个实体命名为Instructor,因为它指导协调员下一步要做什么。

fileprivate enum LaunchInstructor {
  case main, auth, onboarding
  
  static func configure(tutorialWasShown: Bool, isAutorized: Bool) -> LaunchInstructor {
    switch (tutorialWasShown, isAutorized) {
      case (true, false), (false, false): return .auth
      case (false, true): return .onboarding
      case (true, true): return .main
    }
  }
}

创建之后,AppCoordinator可以遵循start函数并根据状态运行不同的流:

override func start() {
  switch instructor {
    case .onboarding: runOnboardingFlow()
    case .auth: runAuthFlow()
    case .main: runMainFlow()
  }
}

此方法适用于任何协调程序。在枚举中提取流逻辑时,代码变得清晰且易于阅读。现在假设我们想要显示ItemCreation模块,但是在显示和一个服务器请求之后,我们收到一个非认证错误。我们如何处理这个错误?协调器只是检查指令并运行验证流。完成后,协调器再次请求指导者,然后运行ItemCreation流。

private func runAuthFlow() {
  let coordinator = coordinatorFactory.makeAuthCoordinatorBox(router: router)
  coordinator.finishFlow = { [weak self, weak coordinator] in
    isAutorized = true
    self?.start() //here we'll check instruction
    self?.removeDependency(coordinator)
  }
  addDependency(coordinator)
  coordinator.start()
}

我相信协调者的优势在于深度链接,推送通知和Force Touch。我不想涉及权利和证书这些东西。假设所有这些都工作正常,我们只需要处理逻辑。如今,当我们打开应用时,我们有很多选择:按压触摸,推送通知,深度链接,或按下应用的图标(也许将来会有新的图标,苹果?🤓)。

avatar

如何处理?如前所述,我介绍了BaseCoordinator,它包含子协调器数组。所有的协调器都继承它。这意味着我们可以循环所有子节点并执行操作。要实现这一点,我们只需添加一个方法来处理DeepLink逻辑本身,或者循环子程序来找到能够处理它的方法。它会像一棵树。我们可以制定具体的协议来实现这个目标。

avatar

让我们更新协调员协议:

protocol Coordinator: class {
  func start()
  func start(with option: DeepLinkOption?)
}

现在,我们只需要将两个主体为空的方法添加到BaseCoordinator,以保持它是可选的。根据我们的目标,孩子可以继承这些方法中的一个,或者两者都继承。在某些情况下,我们可以使用不带选项的start来运行默认行为,但是如果我们继续使用深度链接场景,我们应该添加关于present case的信息。如何实现?更好的方法是在我们的项目中添加一些包含所有深层链接快捷方式的实体,并创建我们可以用**start()**方法发送的对象。

你错过enums吗?让我们添加一个新的!它将是DeepLinkOption:

struct DeepLinkURLConstants {
  static let Onboarding = "onboarding"
  static let Items = "items"
  static let Item = "item"
  static let Settings = "settings"
}

enum DeepLinkOption {
  case onboarding
  case items
  case settings
  case item(String?)
  
  static func build(with id: String, params: [String : AnyObject]?) -> DeepLinkOption? {
    let itemID = params?["item_id"] as? String
    switch id {
      case DeepLinkURLConstants.Onboarding: return .onboarding
      case DeepLinkURLConstants.Items: return .items
      case DeepLinkURLConstants.Item: return .item(itemID)
      case DeepLinkURLConstants.Settings: return .settings
      default: return nil
    }
  }
}

作为一个好处,我们所有的DeepLink方法在协调器将像切换enum。是的,和前面的例子一样,你是正确的。

我们可以根据需要添加一些额外的构建方法:

static func build(with userActivity: NSUserActivity) -> DeepLinkOption?
static func build(with url: URL) -> DeepLinkOption?
static func build(with dict: [String : AnyObject]?) -> DeepLinkOption?

在这个迭代之后,如果我们返回到AppDelegate并更新它一点,它将会像这样:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  let notification = launchOptions?[.remoteNotification] as? [String: AnyObject]
  let deepLink = DeepLinkOption.build(with: notification)
  applicationCoordinator.start(with: deepLink)
  return true
}

func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
  let dict = userInfo as? [String: AnyObject]
  let deepLink = DeepLinkOption.build(with: dict)
  applicationCoordinator.start(with: deepLink)
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
  let deepLink = DeepLinkOption.build(with: userActivity)
  applicationCoordinator.start(with: deepLink)
  return true
}

好吧,看起来不错?🤔 然后您将收到一个推送通知或一个深度链接,您只需构建您的深度链接并使用AppCoordinator继续此操作。

override func start(with option: DeepLinkOption?) {
    //start with deepLink
    if let option = option {
        switch option {
        case .onboarding: runOnboardingFlow()
        case .signUp: runAuthFlow()
        default: childCoordinators.forEach { coordinator in
            coordinator.start(with: option)
            }
        }
    //default start
    } else {
        switch instructor {
        case .onboarding: runOnboardingFlow()
        case .auth: runAuthFlow()
        case .main: runMainFlow()
        }
    }
}

Conclusion

在我们试图在代码中实现的每一个新解决方案中,我们都面临着许多边缘情况和一些不那么漂亮的解决方案。它不应该像炒作驱动的开发,我们只需要适当地逐步完善所有棘手的部分。