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
结构体(例如触发了此路由的前一个屏幕,用于分析目的)。然而,它们的魔法之处在于如何使用:将 Routes
与 RouteHandlers
配对:定义一系列支持的 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
。
要推送一个新的 Feature
,Feature
只需导入包含所需 Route
的模块,并调用 RouterServiceProtocol
的 navigate(_:)
方法。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
和依赖关系,并通过调用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
)
}
}
如果一个禁用的功能尝试在没有回退的情况下显示,您的应用程序将会崩溃。默认情况下,所有功能都已启用并且没有任何回退。
]
所有Routes
都是Codable,但如果后端可以返回多个路由怎么办?
为此,RouterServiceInterface提供了一个类型擦除的AnyRoute
,可以从特定的字符串格式解码任何已注册的Route
。这允许后端指定应用程序内部应该如何处理导航。酷,对吧?
要使用它,请将AnyRoute
(它是Decodable
)添加到后端的响应模型中
struct ProfileResponse: Decodable {
let title: String
let anyRoute: AnyRoute
}
在解码 ProfileResponse
之前,请在 JSONDecoder
中注入您的 RouterService 实例:(必要以确定应该解码哪个路由)
let decoder = JSONDecoder()
routerService.injectContext(toDecoder: decoder)
decoder.decode(ProfileResponse.self, from: data)
现在您可以对 ProfileResponse
进行解码。如果注入的 RouterService 包含后端返回的路由,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'