RouterServiceInterface 1.1.0

RouterServiceInterface 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 应用有 非常快的构建时间基于在 AirBnB 演示的 BA:Swiftable 2019 使用的系统。

RouterService 旨在作为 模块化应用的依赖注入器,其中每个目标包含一个附加的 "interface" 模块。 链接的文章包含了更多关于这个的信息,但简而言之,这个部分基于这样的原则,即功能模块不应该直接依赖于具体模块。相反,出于构建性能的原因,一个功能只能访问另一个功能的 接口,该接口包含不太可能更改的协议和其他东西。为了连接一切,RouterService 负责在引用这些协议时注入必要的具体依赖。

最终结果是

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

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

RouterService 的工作方式

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

RouterService 通过 Feature(功能)的概念工作 – 这是可以在获取所需的任何依赖后创建 ViewControllers 的 struct。以下是一个如何使用该格式创建 "用户简介功能" 的示例。

此功能需要访问 HTTP 客户端,因此我们首先定义它。由于具有 interface 目标的模块化应用应将协议与实现分离,我们的第一个模块将是暴露客户端协议的 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 */ }

我们现在准备定义我们的配置文件路由服务功能。在一个新的 配置文件 模块中,我们可以创建一个具有客户端协议作为依赖的 功能 结构。为了访问协议,配置文件 模块将导入依赖的接口。

// Module 3: Profile

import HTTPClientInterface
import RouterServiceInterface

struct ProfileFeature: Feature {

    @Dependency var client: HTTPClientProtocol

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

由于 配置文件 功能没有导入具体的 HTTPClient 模块,对它们的更改 不会重新编译 配置文件 模块。相反,路由服务将在运行时注入具体对象。如果将其乘以数百个协议和三十几个功能,您将在应用程序中获得极大的编译时间改进!

在这种情况下,只有当接口协议本身发生变化时,配置文件 功能才会由外部更改重新编译——这应该远比更改它们的具体对应物更罕见。

现在,让我们看看如何告诉路由服务推送 配置文件功能 的 ViewController。

路由

在路由服务中,不是通过直接创建 ViewControllers 的实例来推送功能,而是完全通过 路由 进行导航。本身就而言,路由 只是包含有关操作上下文信息的 可编码 结构(例如,触发此路由的前一个屏幕,用于分析目的)。然而,魔法在于它们的使用方式:路由 配对 路由处理器:定义支持 路由 列表和执行它们时应推送 功能 的类的类。

例如,要将上面显示的 配置文件功能 公开给整个应用程序,假设的 配置文件 目标应首先在一个单独的 配置文件接口 目标中暴露路由

// Module 4: ProfileInterface

import RouterServiceInterface

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

现在,在具体的 配置文件 目标中,我们可以定义一个 配置文件路由处理器,将其连接到 配置文件功能

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
    }
}

路由处理器 是为了处理多个 路由 而设计的。如果一个特定的功能目标包含多个 ViewController,您可以在该目标中有一个单个 路由处理器,它可以处理所有可能的路由。

要推送新的 功能功能 只需导入包含所需 路由 的模块,然后调用 RouterServiceProtocolnavigate(_:) 方法。RouterServiceProtocol 是路由服务框架的接口协议,可以为此目的将功能作为依赖项添加。

假设我们伴随着我们的 配置文件功能 一起创建了一些假设的 登录功能,以下是从 配置文件功能 推送 登录功能 的 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和依赖项,并通过调用RouterService的:navigationController(_:)方法启动导航过程循环(如果需要导航),或者通过手动调用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
        )
    }
}

如果禁用的功能尝试展示而没有回退,则您的应用程序会崩溃。默认情况下,所有功能都是启用的,并且没有任何回退。

AnyRoute

所有Routes都是Codable,但如果后端可以返回多个路由怎么办呢?

为此,RouterServiceInterface提供了类型擦除的AnyRoute,可以从特定的字符串格式解码任何注册的Route。这允许后端确定在应用程序内部如何处理导航。酷,对吧?

要使用它,请将AnyRoute(它是Decodable)添加到后端的响应模型中

struct ProfileResponse: Decodable {
    let title: String
    let anyRoute: AnyRoute
}

在解码ProfileResponse之前,将您的RouterService实例注入到JSONDecoder:((必要以确定哪个Route应解码)

let decoder = JSONDecoder()

routerService.injectContext(toDecoder: decoder)

decoder.decode(ProfileResponse.self, from: data)

现在您可以解码ProfileResponse。如果注入的RouterService包含后端返回的Route,则AnyRoute将成功解码到它。

let response = decoder.decode(ProfileResponse.self, from: data)
print(response.anyRoute.route) // Route

框架所期望的字符串格式是route_identifier|parameters_json_string格式的字符串。例如,为了解析本README开头所示的ProfileRouteProfileResponse应如下所示:

{
    "title": "Profile Screen",
    "anyRoute": "profile_mainroute|{\"analyticsContext\": \"Home\"}"
}

安装

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

Swift包管理器

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

CocoaPods

pod 'RouterService'