繼續上一篇的內容:打造完備的 iOS 組件化方案:如何面向接口進行模塊解耦?(一)ios
總結完使用接口進行模塊解耦和依賴管理的方法,咱們能夠進一步對 router 進行擴展了。上面使用 makeDestination
建立模塊是最基本的功能,使用 router 子類後,咱們能夠進行許多有用的功能擴展,這裏給出一些示範。api
編寫 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
方法。bash
把註冊代碼統一管理以後,若是不想使用自動註冊,也能隨時切換爲手動註冊。微信
// editor 模塊的 router
class EditorViewRouter: ZIKViewRouter {
override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView<EditorViewProtocol>())
}
}
複製代碼
<details><summary>Objective-C Sample</summary>網絡
@interface EditorViewRouter : ZIKViewRouter
@end
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[EditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
}
@end
複製代碼
</details>app
iOS 中模塊間耦合的緣由之一,就是界面跳轉的邏輯是經過 UIViewController 進行的,跳轉功能被限制在了 view controller 上,致使數據流經常都繞不開 view 層。要想更好地管理跳轉邏輯,就須要進行封裝。框架
封裝界面跳轉能夠屏蔽 UIKit 的細節,此時界面跳轉的代碼就能夠放在非 view 層(例如 presenter、view model、interactor、service),而且可以跨平臺,也能輕易地經過配置切換跳轉方式。ide
若是是普通的模塊,就用ZIKServiceRouter
,而若是是界面模塊,例如 UIViewController
和 UIView
,就能夠用ZIKViewRouter
,在其中封裝了界面跳轉功能。模塊化
封裝界面跳轉後,使用方式以下:
class TestViewController: UIViewController {
//直接跳轉到 editor 界面
func showEditor() {
Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))
}
//跳轉到 editor 界面,跳轉前用 protocol 配置界面
func prepareAndShowEditor() {
Router.perform(
to: RoutableView<EditorViewProtocol>(),
path: .push(from: self),
preparation: { destination in
// 跳轉前進行配置
// destination 自動推斷爲 EditorViewProtocol
})
}
}
複製代碼
<details><summary>Objective-C Sample</summary>
@implementation TestViewController
- (void)showEditor {
//直接跳轉到 editor 界面
[ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)];
}
- (void)prepareAndShowEditor {
//跳轉到 editor 界面,跳轉前用 protocol 配置界面
[ZIKRouterToView(EditorViewProtocol)
performPath:ZIKViewRoutePath.pushFrom(self)
preparation:^(id<EditorViewProtocol> destination) {
// 跳轉前進行配置
// destination 自動推斷爲 EditorViewProtocol
}];
}
@end
複製代碼
</details>
能夠用 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<EditorViewProtocol>?
func showEditor() {
// 持有 router
router = Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: self))
}
// Router 會對 editor view controller 執行 pop 操做,移除界面
func removeEditor() {
guard let router = router, router.canRemove else {
return
}
router.removeRoute()
router = nil
}
}
複製代碼
<details><summary>Objective-C Sample</summary>
@interface TestViewController()
@property (nonatomic, strong) ZIKDestinationViewRouter(id<EditorViewProtocol>) *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
複製代碼
</details>
有些界面的跳轉方式很特殊,例如 tabbar 上的界面,須要經過切換 tabbar item 來進行。也有的界面有自定義的跳轉動畫,此時能夠在 router 子類中重寫對應方法,進行自定義跳轉。
class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {
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]
}
}
複製代碼
<details><summary>Objective-C Sample</summary>
@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
複製代碼
</details>
不少項目使用了 storyboard,在進行模塊化時,確定不能要求全部使用 storyboard 的模塊都改成使用代碼。所以咱們能夠 hook 一些 storyboard 相關的方法,例如-prepareSegue:sender:
,在其中調用prepareDestination:configuring:
便可。
雖然以前列出了 URL 路由的許多缺點,可是若是你的模塊須要從 h5 界面調用,例如電商 app 須要實現跨平臺的動態路由規則,那麼 URL 路由就是最佳的方案。
可是咱們並不想爲了實現 URL 路由,使用另外一套框架再從新封裝一次模塊。只須要在 router 上擴展 URL 路由的功能,便可同時用接口和 URL 管理模塊。
你能夠給 router 註冊 url:
class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ViewRouteConfig> {
override class func registerRoutableDestination() {
// 註冊 url
registerURLPattern("app://editor/:title")
}
}
複製代碼
<details><summary>Objective-C Sample</summary>
@implementation EditorViewRouter
+ (void)registerRoutableDestination {
// 註冊 url
[self registerURLPattern:@"app://editor/:title"];
}
@end
複製代碼
</details>
以後就能夠用相應的 url 獲取 router:
ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))
複製代碼
<details><summary>Objective-C Sample</summary>
[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];
複製代碼
</details>
以及處理 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
}
複製代碼
<details><summary>Objective-C Sample</summary>
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if ([ZIKAnyViewRouter performURL:urlString fromSource:self.rootViewController]) {
return YES;
} else if ([ZIKAnyServiceRouter performURL:urlString]) {
return YES;
}
return NO;
}
複製代碼
</details>
每一個 router 子類還能各自對 url 進行進一步處理,例如處理 url 中的參數、經過 url 執行對應方法、執行路由後發送返回值給調用者等。
每一個項目對 URL 路由的需求都不同,基於 ZIKRouter 強大的可擴展性,你也能夠按照項目需求實現本身的 URL 路由規則。
除了建立 router 子類,也可使用通用的 router 實例對象,在每一個對象的 block 屬性中提供和 router 子類同樣的功能,所以沒必要擔憂類過多的問題。原理就和用泛型 configuration 代替 configuration 子類同樣。
ZIKViewRoute 對象經過 block 屬性實現子類重寫的效果,代碼能夠用鏈式調用:
ZIKViewRoute<EditorViewController, ViewRouteConfig>
.make(withDestination: EditorViewController.self, makeDestination: ({ (config, router) -> EditorViewController? in
return EditorViewController()
}))
.prepareDestination({ (destination, config, router) in
}).didFinishPrepareDestination({ (destination, config, router) in
})
.register(RoutableView<EditorViewProtocol>())
複製代碼
<details><summary>Objective-C Sample</summary>
[ZIKDestinationViewRoute(id<EditorViewProtocol>)
makeRouteWithDestination:[ZIKInfoViewController class]
makeDestination:^id<EditorViewProtocol> _Nullable(ZIKViewRouteConfig *config, ZIKRouter *router) {
return [[EditorViewController alloc] init];
}]
.prepareDestination(^(id<EditorViewProtocol> destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {
})
.didFinishPrepareDestination(^(id<EditorViewProtocol> destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {
})
.registerDestinationProtocol(ZIKRoutable(EditorViewProtocol));
複製代碼
</details>
基於 ZIKViewRoute 對象實現的 router,能夠進一步簡化 router 的實現代碼。
若是你的類很簡單,並不須要用到 router 子類,直接一行代碼註冊類便可:
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), forMakingView: EditorViewController.self)
複製代碼
<details><summary>Objective-C Sample</summary>
[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewProtocol) forMakingView:[EditorViewController class]];
複製代碼
</details>
或者用 block 自定義建立對象的方式:
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),
forMakingView: EditorViewController.self) { (config, router) -> EditorViewProtocol? in
return EditorViewController()
}
複製代碼
<details><summary>Objective-C Sample</summary>
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewProtocol)
forMakingView:[EditorViewController class]
making:^id _Nullable(ZIKViewRouteConfiguration *config, ZIKViewRouter *router) {
return [[EditorViewController alloc] init];
}];
複製代碼
</details>
或者指定用 C 函數建立對象:
function makeEditorViewController(config: ViewRouteConfig) -> EditorViewController? {
return EditorViewController()
}
ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(),
forMakingView: EditorViewController.self, making: makeEditorViewController)
複製代碼
<details><summary>Objective-C Sample</summary>
id<EditorViewController> makeEditorViewController(ZIKViewRouteConfiguration *config) {
return [[EditorViewController alloc] init];
}
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewProtocol)
forMakingView:[EditorViewController class]
factory:makeEditorViewController];
複製代碼
</details>
有時候模塊須要處理一些系統事件或者 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)
}
}
}
}
複製代碼
<details><summary>Objective-C Sample</summary>
@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
複製代碼
</details>
藉助於使用接口管理依賴的方案,咱們在對模塊進行單元測試時,能夠自由配置 mock 依賴,並且無需 hook 模塊內部的代碼。
例如這樣一個依賴於網絡模塊的登錄模塊:
// 登陸模塊
class LoginService {
func login(account: String, password: String, completion: (Result<LoginError>) -> Void) {
// 內部使用 RequiredNetServiceInput 進行網絡訪問
let netService = Router.makeDestination(to: RoutableService<RequiredNetServiceInput
>())
let request = makeLoginRequest(account: account, password: password)
netService?.POST(request: request, completion: completion)
}
}
// 聲明依賴
extension RoutableService where Protocol == RequiredNetServiceInput {
init() {}
}
複製代碼
<details><summary>Objective-C Sample</summary>
// 登陸模塊
@interface LoginService : NSObject
@end
@implementation LoginService
- (void)loginWithAccount:(NSString *)account password:(NSString *)password completion:(void(^)(Result *result))completion {
// 內部使用 RequiredNetServiceInput 進行網絡訪問
id<RequiredNetServiceInput> netService = [ZIKRouterToService(RequiredNetServiceInput) makeDestination];
Request *request = makeLoginRequest(account, password);
[netService POSTRequest:request completion: completion];
}
@end
// 聲明依賴
@protocol RequiredNetServiceInput <ZIKServiceRoutable>
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion;
@end
複製代碼
</details>
在編寫單元測試時,不須要引入真實的網絡模塊,能夠提供一個自定義的 mock 網絡模塊:
class MockNetService: RequiredNetServiceInput {
func POST(request: Request, completion: (Result<NetError>) {
completion(.success)
}
}
複製代碼
// 註冊 mock 依賴
ZIKAnyServiceRouter.register(RoutableService<RequiredNetServiceInput>(),
forMakingService: MockNetService.self) { (config, router) -> EditorViewProtocol? in
return MockNetService()
}
複製代碼
<details><summary>Objective-C Sample</summary>
@interface MockNetService : NSObject <RequiredNetServiceInput>
@end
@implementation MockNetService
- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion {
completion([Result success]);
}
@end
複製代碼
// 註冊 mock 依賴
[ZIKServiceRouter registerServiceProtocol:ZIKRoutable(EditorViewInput) forMakingService:[MockNetService class]];
複製代碼
</details>
對於那些沒有接口交互的外部依賴,例如只是簡單的跳轉到對應界面,則只需註冊一個空白的 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)}})
}
}
複製代碼
<details><summary>Objective-C Sample</summary>
@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
複製代碼
</details>
使用接口管理依賴,能夠更容易 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<EditorViewController, ViewRouteConfig> {
override class func registerRoutableDestination() {
registerView(EditorViewController.self)
register(RoutableView<EditorViewProtocol>())
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<EditorStorageServiceInput>())
// 其餘配置
// 處理來自 url 的參數
if let title = configuration.userInfo["title"] as? String {
destination.title = title
} else {
destination.title = "默認標題"
}
}
// 事件處理
@objc class func applicationDidEnterBackground(_ application: UIApplication) {
// handle applicationDidEnterBackground event
}
}
複製代碼
<details><summary>Objective-C Sample</summary>
// 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
複製代碼
</details>
咱們能夠看到基於接口管理模塊的優點:
回過頭看以前的 8 個解耦指標,ZIKRouter 已經徹底知足。而 router 提供的多種模塊管理方式(makeDestination、prepareDestination、依賴注入、頁面跳轉、storyboard 支持),可以覆蓋大多數現有的場景,從而實現漸進式的模塊化,減輕重構現有代碼的成本。
書籍目錄——獲取地址加小編微信拉你進iOS開發羣:17512010526
做者:黑超熊貓zuik