RouterService 1.1.0

RouterService 1.1.0

Bruno Rocha 维护。



RouterService

struct SwiftRocksFeature: Feature {

    @Dependency var client: HTTPClientProtocol
    @Dependency var persistence: PersistenceProtocol
    @Dependency var routerService: RouterServiceProtocol

    func build(fromRoute route: Route?) -> UIViewController {
        return SwiftRocksViewController(
            client: client,
            persistence: persistence,
            routerService: routerService,
        )
    }
}

RouterService是一个类型安全的导航/依赖注入框架,专注于使模块化Swift应用具有**非常快的构建时间**。基于在BA:Swiftable 2019上展示的AirBnB使用的系统。基于AirBnB使用的系统,在BA:Swiftable 2019上展示。

RouterService旨在用作依赖注入器,用于模块化应用,其中每个目标包含一个额外的"接口"模块。 相关文章包含更多信息,但简要来说,这部分是基于这样一个原则:特性模块不应直接依赖具体模块。而是出于构建性能原因,特性只能访问其他特性的**接口**,其中包含不太可能改变协议和其他内容。为了连接一切,RouterService会在需要引用这些协议时注入必要的具体依赖。

最终结果是

  • 具有水平依赖图的程序(非常快的构建时间!)
  • 动态导航(任何屏幕都可以从任何地方推送!)

有关此架构的更多信息,请查看相关的SwiftRocks文章。

RouterService工作原理

(要查看完整的示例,请检查此存储库的示例应用。)

RouterService通过“特性”的概念来工作 -- 这是可以在提供需要用于该任务的任何依赖项后创建视图控制器的struct。以下是我们如何使用此格式创建“用户资料特性”的示例。

此特性需要访问HTTP客户端,因此我们将首先定义它。由于具有接口目标的模块化应用应将协议与实现分开,我们的第一个模块将是HTTPClientInterface,它公开客户端协议

// Module 1: HTTPClientInterface

public protocol HTTPClientProtocol: AnyObject { /* Client stuff */ }

从界面来看,现在我们来创建一个实施了它的 具体 HTTPClient 模块。

// Module 2: HTTPClient

import HTTPClientInterface

private class HTTPClient: HTTPClientProtocol { /* Implementation of the client stuff */ }

现在我们已准备好定义我们的 Profile RouterService 功能。在一个新的 Profile 模块中,我们可以创建一个具有客户端协议作为依赖的 Feature 结构体。为了访问协议,Profile 模块将导入依赖的接口。

// Module 3: Profile

import HTTPClientInterface
import RouterServiceInterface

struct ProfileFeature: Feature {

    @Dependency var client: HTTPClientProtocol

    func build(fromRoute route: Route?) -> UIViewController {
        return ProfileViewController(client: client)
    }
}

由于 Profile 功能没有导入具体的 HTTPClient 模块,对它们的改动 不会重新编译 Profile 模块。相反,RouterService 将在运行时注入具体对象。如果您有数百个协议和数十个功能,您将看到在您的应用程序中实现了巨大的构建时间改进!

在这种情况下,如果接口协议本身发生变化,才会导致 Profile 功能被重新编译。这应该比它们的具体系列变更难得多。

现在让我们看看如何让 RouterService 推送 ProfileFeature 的 ViewController。

路由

在 RouterService 中,不是通过直接创建它们的 ViewController 实例来推送功能,导航完全通过 Routes 完成。就其本身而言,Routes 只是能够携带关于某个动作的上下文信息的 Codable 结构体(例如触发了此路由的前一个屏幕,用于分析目的)。然而,它们的魔法之处在于如何使用:将 RoutesRouteHandlers 配对:定义一系列支持的 Routes 以及在这些动作执行时应该推送哪个 Features 的类。

例如,要向整个应用程序公开上面所示的 ProfileFeature,假设的 Profile 目标应首先在一个单独的 ProfileInterface 目标中公开一个路由。

// Module 4: ProfileInterface

import RouterServiceInterface

struct ProfileRoute: Route {
    static let identifier: String = "profile_mainroute"
    let someAnalyticsContext: String
}

现在,在具体的 Profile 目标中,我们可以定义一个 ProfileRouteHandler 以连接到 ProfileFeature

import ProfileInterface
import RouterServiceInterface

public final class ProfileRouteHandler: RouteHandler {
    public var routes: [Route.Type] {
        return [ProfileRoute.self]
    }

    public func destination(
        forRoute route: Route,
        fromViewController viewController: UIViewController
    ) -> Feature.Type {
        guard route is ProfileRoute else {
            preconditionFailure() // unexpected route sent to this handler
        }
        return ProfileFeature.self
    }
}

RouteHandlers 被设计成处理多个 Routes。如果一个特定的功能目标包含多个 ViewController,可以在该目标中有一个单独的 RouteHandler 来处理所有可能的 Routes

要推送一个新的 FeatureFeature 只需导入包含所需 Route 的模块,并调用 RouterServiceProtocolnavigate(_:) 方法。RouterServiceProtocol 是 RouterService 框架的接口协议,可以将其作为依赖添加到功能中为此目的。

假设我们还创建了一个假设的 LoginFeature 作为我们的 ProfileFeature 一同,这是我们从 ProfileFeature 推送 LoginFeature 的 ViewController 的方法。

import LoginInterface
import RouterServiceInterface
import HTTPClientInterface
import UIKit

final class ProfileViewController: UIViewController {
    let client: HTTPClientProtocol
    let routerService: RouterServiceProtocol

    init(client: HTTPClientProtocol, routerService, RouterServiceProtocol) {
        self.client = client
        self.routerService = routerService
        super.init(nibName: nil, bundle: nil)
    }

    func goToLogin() {
        let loginRoute = SomeLoginRouteFromTheLoginFeatureInterface()
        dependencies.routerService.navigate(
            toRoute: loginRoute,
            fromView: self,
            presentationStyle: Push(),
            animated: true
        )
    }
}

总结一切

如果所有功能都是孤立的,您如何启动应用程序?

尽管功能与其他具体目标隔离,您应该有一个“主要”目标,它导入一切和所有人(例如,您的AppDelegate)。这应该是唯一能够导入具体目标的目標。

从那里,您可以创建一个RouterService的具体实例,注册每个人的RouteHandlers和依赖关系,并通过调用RouterServicenavigationController(_:)方法(如果需要导航)或手动调用Featurebuild(_:)方法来启动导航过程循环。

import HTTPClient
import Profile
import Login

class AppDelegate {

   let routerService = RouterService()

   func didFinishLaunchingWith(...) {

       // Register Dependencies

       routerService.register(dependencyFactory: { 
           return HTTPClient() 
       }, forType: HTTPClientProtocol.self)

       // Register RouteHandlers

       routerService.register(routeHandler: ProfileRouteHandler())
       routerService.register(routeHandler: LoginRouteHandler())

       // Setup Window

       let window = UIWindow()
       window.makeKeyAndVisible()

       // Start RouterService

       window.rootViewController = routerService.navigationController(
        withInitialFeature: ProfileFeature.self
       )

       return true
   }
}

有关更多信息和示例,请查看此仓库中提供的示例应用。它包含一个具有两个功能目标和一个模拟依赖目标的应用。

依赖的内存管理

通过闭包(称为“依赖工厂”)注册依赖项,允许RouterService按需生成它们的实例,并在没有需要它们的任何功能时释放它们。这是通过有一个内部存储来实现的,它弱引用地保留值。闭包本身在整个应用程序的生命周期内保留在内存中,但应该比保留实例本身的影响小。

为功能提供回退方案

如果您需要控制您功能的可用性,无论是由于功能标志还是因为它具有最低iOS版本要求,您可以通过FeatureisEnabled()方法来处理它。此方法向RouterService提供有关您功能可用性的信息。我们强烈建议您将切换控制(功能标志提供者、远程配置提供者、用户默认值等)作为您的功能的@Dependency,这样可以轻松使用它们来实现它,并在以后正确地对它进行单元测试。如果一个Feature可以禁用,您需要通过实现fallback(_:)方法提供回退,以便RouterService接收和显示有效的上下文。例如

public struct FooFeature: Feature {

    @Dependency var httpClient: HTTPClientProtocol
    @Dependency var routerService: RouterServiceProtocol
    @Dependency var featureFlag: FeatureFlagProtocol

    public init() {}
    
    public func isEnabled() -> Bool {
        return featureFlag.isEnabled()
    }
    
    public func fallback(forRoute route: Route?) -> Feature.Type? {
        return MyFallbackFeature.self
    }

    public func build(fromRoute route: Route?) -> UIViewController {
        return MainViewController(
            httpClient: httpClient,
            routerService: routerService
        )
    }
}

如果一个禁用的功能尝试在没有回退的情况下显示,您的应用程序将会崩溃。默认情况下,所有功能都已启用并且没有任何回退。

安装

安装 RouterService 时,还会安装接口模块 RouterServiceInterface

Swift 包管理器

.package(url: "https://github.com/rockbruno/RouterService", .upToNextMinor(from: "1.0.0"))

CocoaPods

pod 'RouterService'