打造完備的 iOS 元件化方案:如何面向介面進行模組解耦?(二)
繼續上一篇的內容:打造完備的 iOS 元件化方案:如何面向介面進行模組解耦?(一)
功能擴充套件
總結完使用介面進行模組解耦和依賴管理的方法,我們可以進一步對 router 進行擴充套件了。上面使用 makeDestination
建立模組是最基本的功能,使用 router 子類後,我們可以進行許多有用的功能擴充套件,這裡給出一些示範。
自動註冊
編寫 router 程式碼時,需要註冊 router 和 protocol 。在 OC 中可以在 +load 方法中註冊,但是 Swift 裡已經不能使用 +load 方法,而且分散在 +load 中的註冊程式碼也不好管理。BeeHive 中通過巨集定義和__attribute((used,section("__DATA,""BeehiveServices""")))
我們可以把註冊程式碼寫在 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<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>
封裝介面跳轉
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<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,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,在進行模組化時,肯定不能要求所有使用 storyboard 的模組都改為使用程式碼。因此我們可以 hook 一些 storyboard 相關的方法,例如-prepareSegue:sender:
,在其中呼叫prepareDestination:configuring:
即可。
URL 路由
雖然之前列出了 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 子類
除了建立 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,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,ZIKViewRouter *router) {
})
.registerDestinationProtocol(ZIKRoutable(EditorViewProtocol));
複製程式碼
</details>
簡化 router 實現
基於 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>
基於介面進行解耦的優勢
我們可以看到基於介面管理模組的優勢:
- 依賴編譯檢查,實現嚴格的型別安全
- 依賴編譯檢查,減少重構時的成本
- 通過介面明確宣告模組所需的依賴,允許外部進行依賴注入
- 保持動態特性的同時,進行路由檢查,避免使用不存在的路由模組
- 利用介面,區分 required protocol 和 provided protocol,進行明確的模組適配,實現徹底解耦
回過頭看之前的 8 個解耦指標,ZIKRouter 已經完全滿足。而 router 提供的多種模組管理方式(makeDestination、prepareDestination、依賴注入、頁面跳轉、storyboard 支援),能夠覆蓋大多數現有的場景,從而實現漸進式的模組化,減輕重構現有程式碼的成本。
書籍目錄——獲取地址加小編微信拉你進iOS開發群:17512010526
作者:黑超熊貓zuik
來源:www.jianshu.com/p/3aab83626…