继续上一篇的内容:打造完备的 iOS 组件化方案:如何面向接口进行模块解耦?(一)
功能扩展
总结完使用接口进行模块解耦和依赖管理的方法,我们可以进一步对 router 进行扩展了。上面使用 makeDestination
创建模块是最基本的功能,使用 router 子类后,我们可以进行许多有用的功能扩展,这里给出一些示范。
自动注册
编写 router 代码时,需要注册 router 和 protocol 。在 OC 中可以在 +load 方法中注册,但是 Swift 里已经不能使用 +load 方法,而且分散在 +load 中的注册代码也不好管理。BeeHive 中通过宏定义和__attribute((used, section("__DATA,""BeehiveServices""")))
,把注册信息添加到了 mach-O 中的自定义区域,然后在启动时读取并自动注册,可惜这种方式在 Swift 中也无法使用了。
我们可以把注册代码写在 router 的+registerRoutableDestination
方法里,然后逐个调用每个 router 类的+registerRoutableDestination
方法即可。还可以更进一步,用 runtime 技术遍历 mach-O 中的__DATA,__objc_classlist
区域的类列表,获取所有的 router 类,自动调用所有的+registerRoutableDestination
方法。
把注册代码统一管理之后,如果不想使用自动注册,也能随时切换为手动注册。
// editor 模块的 router
class EditorViewRouter: ZIKViewRouter {
override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView())
}
}
Objective-C Sample
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[EditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
}
@end
封装界面跳转
iOS 中模块间耦合的原因之一,就是界面跳转的逻辑是通过 UIViewController 进行的,跳转功能被限制在了 view controller 上,导致数据流常常都绕不开 view 层。要想更好地管理跳转逻辑,就需要进行封装。
封装界面跳转可以屏蔽 UIKit 的细节,此时界面跳转的代码就可以放在非 view 层(例如 presenter、view model、interactor、service),并且能够跨平台,也能轻易地通过配置切换跳转方式。
如果是普通的模块,就用ZIKServiceRouter
,而如果是界面模块,例如 UIViewController
和 UIView
,就可以用ZIKViewRouter
,在其中封装了界面跳转功能。
封装界面跳转后,使用方式如下:
class TestViewController: UIViewController {
//直接跳转到 editor 界面
func showEditor() {
Router.perform(to: RoutableView(), path: .push(from: self))
}
//跳转到 editor 界面,跳转前用 protocol 配置界面
func prepareAndShowEditor() {
Router.perform(
to: RoutableView(),
path: .push(from: self),
preparation: { destination in
// 跳转前进行配置
// destination 自动推断为 EditorViewProtocol
})
}
}
Objective-C Sample
@implementation TestViewController
- (void)showEditor {
//直接跳转到 editor 界面
[ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}
- (void)prepareAndShowEditor {
//跳转到 editor 界面,跳转前用 protocol 配置界面
[ZIKRouterToView(EditorViewProtocol)
performPath:ZIKViewRoutePath.pushFrom(self)
preparation:^(id destination) {
// 跳转前进行配置
// destination 自动推断为 EditorViewProtocol
}];
}
@end
可以用 ViewRoutePath
一键切换不同的跳转方式:
enum ViewRoutePath {
case push(from: UIViewController)
case presentModally(from: UIViewController)
case presentAsPopover(from: UIViewController, configure: ZIKViewRoutePopoverConfigure)
case performSegue(from: UIViewController, identifier: String, sender: Any?)
case show(from: UIViewController)
case showDetail(from: UIViewController)
case addAsChildViewController(from: UIViewController, addingChildViewHandler: (UIViewController, @escaping () -> Void) -> Void)
case addAsSubview(from: UIView)
case custom(from: ZIKViewRouteSource?)
case makeDestination
case extensible(path: ZIKViewRoutePath)
}
而且在界面跳转后,还可以根据跳转时的跳转方式,一键回退界面,无需再手动区分 dismiss、pop 等各种情况:
class TestViewController: UIViewController {
var router: DestinationViewRouter?
func showEditor() {
// 持有 router
router = Router.perform(to: RoutableView(), path: .push(from: self))
}
// Router 会对 editor view controller 执行 pop 操作,移除界面
func removeEditor() {
guard let router = router, router.canRemove else {
return
}
router.removeRoute()
router = nil
}
}
Objective-C Sample
@interface TestViewController()
@property (nonatomic, strong) ZIKDestinationViewRouter(id) *router;
@end
@implementation TestViewController
- (void)showEditor {
// 持有 router
self.router = [ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}
// Router 会对 editor view controller 执行 pop 操作,移除界面
- (void)removeEditor {
if (![self.router canRemove]) {
return;
}
[self.router removeRoute];
self.router = nil;
}
@end
自定义跳转
有些界面的跳转方式很特殊,例如 tabbar 上的界面,需要通过切换 tabbar item 来进行。也有的界面有自定义的跳转动画,此时可以在 router 子类中重写对应方法,进行自定义跳转。
class EditorViewRouter: ZIKViewRouter {
override func destination(with configuration: ViewRouteConfig) -> Any? {
return EditorViewController()
}
override func canPerformCustomRoute() -> Bool {
return true
}
override func performCustomRoute(onDestination destination: EditorViewController, fromSource source: Any?, configuration: ViewRouteConfig) {
beginPerformRoute()
// 自定义跳转
CustomAnimator.transition(from: source, to: destination) {
self.endPerformRouteWithSuccess()
}
}
override func canRemoveCustomRoute() -> Bool {
return true
}
override func removeCustomRoute(onDestination destination: EditorViewController, fromSource source: Any?, removeConfiguration: ViewRemoveConfig, configuration: ViewRouteConfig) {
beginRemoveRoute(fromSource: source)
// 移除自定义跳转
CustomAnimator.dismiss(destination) {
self.endRemoveRouteWithSuccess(onDestination: destination, fromSource: source)
}
}
override class func supportedRouteTypes() -> ZIKViewRouteTypeMask {
return [.custom, .viewControllerDefault]
}
}
Objective-C Sample
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
return [[EditorViewController alloc] init];
}
- (BOOL)canPerformCustomRoute {
return YES;
}
- (void)performCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source configuration:(ZIKViewRouteConfiguration *)configuration {
[self beginPerformRoute];
// 自定义跳转
[CustomAnimator transitionFrom:source to:destination completion:^{
[self endPerformRouteWithSuccess];
}];
}
- (BOOL)canRemoveCustomRoute {
return YES;
}
- (void)removeCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source removeConfiguration:(ZIKViewRemoveConfiguration *)removeConfiguration configuration:(__kindof ZIKViewRouteConfiguration *)configuration {
[self beginRemoveRouteFromSource:source];
// 移除自定义跳转
[CustomAnimator dismiss:destination completion:^{
[self endRemoveRouteWithSuccessOnDestination:destination fromSource:source];
}];
}
+ (ZIKViewRouteTypeMask)supportedRouteTypes {
return ZIKViewRouteTypeMaskCustom|ZIKViewRouteTypeMaskViewControllerDefault;
}
@end
支持 storyboard
很多项目使用了 storyboard,在进行模块化时,肯定不能要求所有使用 storyboard 的模块都改为使用代码。因此我们可以 hook 一些 storyboard 相关的方法,例如-prepareSegue:sender:
,在其中调用prepareDestination:configuring:
即可。
URL 路由
虽然之前列出了 URL 路由的许多缺点,但是如果你的模块需要从 h5 界面调用,例如电商 app 需要实现跨平台的动态路由规则,那么 URL 路由就是最佳的方案。
但是我们并不想为了实现 URL 路由,使用另一套框架再重新封装一次模块。只需要在 router 上扩展 URL 路由的功能,即可同时用接口和 URL 管理模块。
你可以给 router 注册 url:
class EditorViewRouter: ZIKViewRouter {
override class func registerRoutableDestination() {
// 注册 url
registerURLPattern("app://editor/:title")
}
}
Objective-C Sample
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
// 注册 url
[self registerURLPattern:@"app://editor/:title"];
}
@end
之后就可以用相应的 url 获取 router:
ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))
Objective-C Sample
[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];
以及处理 URL Scheme:
public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
let urlString = url.absoluteString
if let _ = ZIKAnyViewRouter.performURL(urlString, fromSource: self.rootViewController) {
return true
} else if let _ = ZIKAnyServiceRouter.performURL(urlString) {
return true
}
return false
}
Objective-C Sample
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
if ([ZIKAnyViewRouter performURL:urlString fromSource:self.rootViewController]) {
return YES;
} else if ([ZIKAnyServiceRouter performURL:urlString]) {
return YES;
}
return NO;
}
每个 router 子类还能各自对 url 进行进一步处理,例如处理 url 中的参数、通过 url 执行对应方法、执行路由后发送返回值给调用者等。
每个项目对 URL 路由的需求都不一样,基于 ZIKRouter 强大的可扩展性,你也可以按照项目需求实现自己的 URL 路由规则。
用 router 对象代替 router 子类
除了创建 router 子类,也可以使用通用的 router 实例对象,在每个对象的 block 属性中提供和 router 子类一样的功能,因此不必担心类过多的问题。原理就和用泛型 configuration 代替 configuration 子类一样。
ZIKViewRoute 对象通过 block 属性实现子类重写的效果,代码可以用链式调用:
ZIKViewRoute
.make(withDestination: EditorViewController.self, makeDestination: ({ (config, router) -> EditorViewController? in
return EditorViewController()
}))
.prepareDestination({ (destination, config, router) in
}).didFinishPrepareDestination({ (destination, config, router) in
})
.register(RoutableView())
Objective-C Sample
[ZIKDestinationViewRoute(id)
makeRouteWithDestination:[ZIKInfoViewController class]
makeDestination:^id _Nullable(ZIKViewRouteConfig *config, ZIKRouter *router) {
return [[EditorViewController alloc] init];
}]
.prepareDestination(^(id destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {
})
.didFinishPrepareDestination(^(id destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {
})
.registerDestinationProtocol(ZIKRoutable(EditorViewProtocol));
简化 router 实现
基于 ZIKViewRoute 对象实现的 router,可以进一步简化 router 的实现代码。
如果你的类很简单,并不需要用到 router 子类,直接一行代码注册类即可:
ZIKAnyViewRouter.register(RoutableView(), forMakingView: EditorViewController.self)
Objective-C Sample
[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewProtocol) forMakingView:[EditorViewController class]];
或者用 block 自定义创建对象的方式:
ZIKAnyViewRouter.register(RoutableView(),
forMakingView: EditorViewController.self) { (config, router) -> EditorViewProtocol? in
return EditorViewController()
}
Objective-C Sample
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewProtocol)
forMakingView:[EditorViewController class]
making:^id _Nullable(ZIKViewRouteConfiguration *config, ZIKViewRouter *router) {
return [[EditorViewController alloc] init];
}];
或者指定用 C 函数创建对象:
function makeEditorViewController(config: ViewRouteConfig) -> EditorViewController? {
return EditorViewController()
}
ZIKAnyViewRouter.register(RoutableView(),
forMakingView: EditorViewController.self, making: makeEditorViewController)
Objective-C Sample
id makeEditorViewController(ZIKViewRouteConfiguration *config) {
return [[EditorViewController alloc] init];
}
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewProtocol)
forMakingView:[EditorViewController class]
factory:makeEditorViewController];
事件处理
有时候模块需要处理一些系统事件或者 app 的自定义事件,此时可以让 router 子类实现,再进行遍历分发。
class SomeServiceRouter: ZIKServiceRouter {
@objc class func applicationDidEnterBackground(_ application: UIApplication) {
// handle applicationDidEnterBackground event
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) {
Router.enumerateAllViewRouters { (routerType) in
if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
routerType.perform(#selector(applicationDidEnterBackground(_:)), with: application)
}
}
Router.enumerateAllServiceRouters { (routerType) in
if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
routerType.perform(#selector(applicationDidEnterBackground(_:)), with: application)
}
}
}
}
Objective-C Sample
@interface SomeServiceRouter : ZIKServiceRouter
@end
@implementation SomeServiceRouter
+ (void)applicationDidEnterBackground:(UIApplication *)application {
// handle applicationDidEnterBackground event
}
@end
@interface AppDelegate ()
@end
@implementation AppDelegate
- (void)applicationDidEnterBackground:(UIApplication *)application {
[ZIKAnyViewRouter enumerateAllViewRouters:^(Class routerClass) {
if ([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) {
[routerClass applicationDidEnterBackground:application];
}
}];
[ZIKAnyServiceRouter enumerateAllServiceRouters:^(Class routerClass) {
if ([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) {
[routerClass applicationDidEnterBackground:application];
}
}];
}
@end
单元测试
借助于使用接口管理依赖的方案,我们在对模块进行单元测试时,可以自由配置 mock 依赖,而且无需 hook 模块内部的代码。
例如这样一个依赖于网络模块的登陆模块:
// 登录模块
class LoginService {
func login(account: String, password: String, completion: (Result) -> Void) {
// 内部使用 RequiredNetServiceInput 进行网络访问
let netService = Router.makeDestination(to: RoutableService())
let request = makeLoginRequest(account: account, password: password)
netService?.POST(request: request, completion: completion)
}
}
// 声明依赖
extension RoutableService where Protocol == RequiredNetServiceInput {
init() {}
}
Objective-C Sample
// 登录模块
@interface LoginService : NSObject
@end
@implementation LoginService
- (void)loginWithAccount:(NSString *)account password:(NSString *)password completion:(void(^)(Result *result))completion {
// 内部使用 RequiredNetServiceInput 进行网络访问
id netService = [ZIKRouterToService(RequiredNetServiceInput) makeDestination];
Request *request = makeLoginRequest(account, password);
[netService POSTRequest:request completion: completion];
}
@end
// 声明依赖
@protocol RequiredNetServiceInput
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion;
@end
在编写单元测试时,不需要引入真实的网络模块,可以提供一个自定义的 mock 网络模块:
class MockNetService: RequiredNetServiceInput {
func POST(request: Request, completion: (Result) {
completion(.success)
}
}
// 注册 mock 依赖
ZIKAnyServiceRouter.register(RoutableService(),
forMakingService: MockNetService.self) { (config, router) -> EditorViewProtocol? in
return MockNetService()
}
Objective-C Sample
@interface MockNetService : NSObject
@end
@implementation MockNetService
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion {
completion([Result success]);
}
@end
// 注册 mock 依赖
[ZIKServiceRouter registerServiceProtocol:ZIKRoutable(EditorViewInput) forMakingService:[MockNetService class]];
对于那些没有接口交互的外部依赖,例如只是简单的跳转到对应界面,则只需注册一个空白的 proxy。
单元测试代码:
class LoginServiceTests: XCTestCase {
func testLoginSuccess() {
let expectation = expectation(description: "end login")
let loginService = LoginService()
loginService.login(account: "account", password: "pwd") { result in
expectation.fulfill()
}
waitForExpectations(timeout: 5, handler: { if let error = $0 {print(error)}})
}
}
Objective-C Sample
@interface LoginServiceTests : XCTestCase
@end
@implementation LoginServiceTests
- (void)testLoginSuccess {
XCTestExpectation *expectation = [self expectationWithDescription:@"end login"];
[[LoginService new] loginWithAccount:@"" password:@"" completion:^(Result *result) {
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) {
!error? : NSLog(@"%@", error);
}];
}
@end
使用接口管理依赖,可以更容易 mock,剥除外部依赖对测试的影响,让单元测试更稳定。
接口版本管理
使用接口管理模块时,还有一个问题需要注意。接口是会随着模块更新而变化的,这个接口已经被很多外部使用了,要如何减少接口变化产生的影响?
此时需要区分新接口和旧接口,区分版本,推出新接口的同时,保留旧接口,并将旧接口标记为废弃。这样使用者就可以暂时使用旧接口,渐进式地修改代码。
这部分可以参考 Swift 和 OC 中的版本管理宏。
接口废弃,可以暂时使用,建议尽快使用新接口代替:
// Swift
@available(iOS, deprecated: 8.0, message: "Use new interface instead")
// Objective-C
API_DEPRECATED_WITH_REPLACEMENT("performPath:configuring:", ios(7.0, 7.0));
接口已经无效:
// Swift
@available(iOS, unavailable)
// Objective-C
NS_UNAVAILABLE
最终形态
最后,一个 router 的最终形态就是下面这样:
// editor 模块的 router
class EditorViewRouter: ZIKViewRouter {
override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView())
registerURLPattern("app://editor/:title")
}
override func processUserInfo(_ userInfo: [AnyHashable : Any] = [:], from url: URL) {
let title = userInfo["title"]
// 处理 url 中的参数
}
// 子类重写,创建模块
override func destination(with configuration: ViewRouteConfig) -> Any? {
let destination = EditorViewController()
return destination
}
// 配置模块,注入静态依赖
override func prepareDestination(_ destination: EditorViewController, configuration: ViewRouteConfig) {
// 注入 service 依赖
destination.storageService = Router.makeDestination(to: RoutableService())
// 其他配置
// 处理来自 url 的参数
if let title = configuration.userInfo["title"] as? String {
destination.title = title
} else {
destination.title = "默认标题"
}
}
// 事件处理
@objc class func applicationDidEnterBackground(_ application: UIApplication) {
// handle applicationDidEnterBackground event
}
}
Objective-C Sample
// editor 模块的 router
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[EditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
[self registerURLPattern:@"app://editor/:title"];
}
- (void)processUserInfo:(NSDictionary *)userInfo fromURL:(NSURL *)url {
NSString *title = userInfo[@"title"];
// 处理 url 中的参数
}
// 子类重写,创建模块
- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
EditorViewController *destination = [[EditorViewController alloc] init];
return destination;
}
// 配置模块,注入静态依赖
- (void)prepareDestination:(EditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
// 注入 service 依赖
destination.storageService = [ZIKRouterToService(EditorStorageServiceInput) makeDestination];
// 其他配置
// 处理来自 url 的参数
NSString *title = configuration.userInfo[@"title"];
if (title) {
destination.title = title;
} else {
destination.title = @"默认标题";
}
}
// 事件处理
+ (void)applicationDidEnterBackground:(UIApplication *)application {
// handle applicationDidEnterBackground event
}
@end
基于接口进行解耦的优势
我们可以看到基于接口管理模块的优势:
- 依赖编译检查,实现严格的类型安全
- 依赖编译检查,减少重构时的成本
- 通过接口明确声明模块所需的依赖,允许外部进行依赖注入
- 保持动态特性的同时,进行路由检查,避免使用不存在的路由模块
- 利用接口,区分 required protocol 和 provided protocol,进行明确的模块适配,实现彻底解耦
回过头看之前的 8 个解耦指标,ZIKRouter 已经完全满足。而 router 提供的多种模块管理方式(makeDestination、prepareDestination、依赖注入、页面跳转、storyboard 支持),能够覆盖大多数现有的场景,从而实现渐进式的模块化,减轻重构现有代码的成本。