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 旨在用作 模块化应用(其中每个目标包含一个额外的“接口”模块)的依赖注入器。 链接的文章包含更多关于这方面的信息,但作为总结,这部分源于一个原则,即功能模块永远不应直接依赖于具体的模块。 相反,出于构建性能的原因,一个功能只能访问另一个功能的接口,其中包含协议和其他不太可能更改的内容。 为了将所有内容连接在一起,RouterService 负责在引用这些协议之一时注入必要的具体依赖项。
最终结果是
有关此架构的更多信息,请查看相关的 SwiftRocks 文章。
(有关完整示例,请查看此仓库的示例应用。)
RouterService 通过 Features
的概念工作 -- Features
是 structs
,可以在被赋予访问执行此操作所需的任何依赖项后创建 ViewControllers。 这是一个关于我们如何使用这种格式创建一个“用户个人资料功能”的示例。
此功能需要访问 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 中,导航不是通过直接创建 Features 的 ViewControllers 实例来完成的,而是完全通过 Routes
完成的。 就其本身而言,Routes
只是 Codable
结构,可以保存关于操作的上下文信息(例如触发此路由的先前屏幕,用于分析目的)。 然而,神奇之处在于它们的使用方式:Routes
与 RouteHandlers
配对: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
。 如果一个特定的功能目标包含多个 ViewControllers,您可以在该目标中拥有一个单独的 RouteHandler
来处理所有可能的 Routes
。
为了推送一个新的 Feature
,Feature
所要做的就是导入包含所需 Route
的模块,并调用 RouterServiceProtocol
的 navigate(_:)
方法。 RouterServiceProtocol
,RouterService 框架的接口协议,可以作为功能的依赖项添加,用于此目的。
假设我们也在 ProfileFeature
的同时创建了一些假设的 LoginFeature
,以下是我们如何从 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()
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
之前,将您的 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
也将被安装。
.package(url: "https://github.com/rockbruno/RouterService", .upToNextMinor(from: "1.1.0"))
pod 'RouterService'
ExampleProject 使用 XcodeGen 来生成其 Xcode 项目。
您可以选择使用 Homebrew 安装 XcodeGen 二进制文件,或者利用 Swift Package Manager,它将克隆、构建然后运行 XcodeGen。 下面概述了生成 RouterServiceExampleApp.xcodeproj
的各个步骤。
# 1. Install XcodeGen using Homebrew
brew install xcodegen
# 2. Run xcodegen from within the ExampleProject directory
cd ExampleProject && xcodegen generate
# 1. Run xcodegen using SPM from within the ExampleProject directory
cd ExampleProject && swift run xcodegen