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,您可以在该目标中有一个单个 路由处理器
,它可以处理所有可能的路由。
要推送新的 功能
,功能
只需导入包含所需 路由
的模块,然后调用 RouterServiceProtocol
的 navigate(_:)
方法。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(_:)
方法启动导航过程循环(如果需要导航),或者通过手动调用Feature
的build(_:)
方法。
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版本要求,都可以通过一个Feature
的isEnabled()
方法来处理。此方法向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开头所示的ProfileRoute
,ProfileResponse
应如下所示:
{
"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'