iOS项目组件化历程
為什么要組件化
隨著業務的發展,App中的頁面,網絡請求,通用彈層UI,通用TableCell數量就會劇增,需求的開發人員數量也會逐漸增多。
如果所有業務都在同一個App中,并且同時開發人數較少時,拋開代碼健壯性不談,實際的開發體驗可能并沒有那么糟糕,畢竟作為一個開發,什么地方用什么控件,就跟在HashMap中通過Key獲取Value那么簡單。
那么當業務成長到需要分化到多個App的時候,組件化的重要性開始體現了。
展示控件
@interface CESettingsCell : UITableViewCell@property (strong, nonatomic) UILabel *titleLabel; @property (strong, nonatomic) UILabel *tipsLabel; @property (strong, nonatomic) UIImageView *arrowImgV;@end 復制代碼如代碼所示這是一個很常見TableCell,其中有標題,小圖標,右箭頭。將這樣的組件抽象成一個基類,后續再使用的時候,就可以直接繼承改寫,或者直接使用,能省去很多工作量。
隨著頁面的增加,這種結構會被大量的運用在其他列表之中。其實在第二相似需求出現的時候,就該考慮進行抽象的,可惜經常是忙于追趕業務,寫著寫著就給忘記了。
交互控件
@interface CEOptionPickerViewController : CEBaseViewController@property (strong, nonatomic) NSArray<NSArray *> *pickerDataList; @property (strong, nonatomic) NSMutableArray<NSNumber *> *selectedIndexList; @property (strong, nonatomic) NSString *tipsTitle;@property (strong, nonatomic) NSDictionary *rowAttributes;@property (copy, nonatomic) void(^didOptionSelectedBlock) (NSArray<NSNumber *> *selectedIndexList);@end 復制代碼這也是一個已經抽象好的控件,作用是顯示一個內容為二維數組的選擇器,可以用來選擇省份-城市,或者年-月
這種類型的數據。
在組件中,這類一次編寫,多場景使用組件是最容易抽象的,一般在第一次開發的時候就能想到組件化。需要注意的是,這樣的組件盡量不要使用多層繼承,如果有相同特性但是不同的實現,用Protocal將它們抽象出來。
牢記Copy-Paste是埋坑的開始(哈哈哈哈哈,你會忘記哪一份代碼是最新的,血淚教訓)。
基類與Category
基類并不雞肋,合理使用,可以減少很多的重復代碼,比如ViewController對StatusBar的控制,NavigationController對NavBar的控制。
這種全局都可能會用到的方法適合抽象到基類或Category中,避免重復代碼。在抽象方法的時候一定要克制,確認影響范圍足夠廣,實現方式比較普遍的實現才適合放入基類中,與業務相關的代碼更需要酌情考慮。
比如一個定制化的返回鍵,在當前項目中屬于通用方案,每個導航欄頁面都用到了,但是如果新開了一個項目,是否是改個圖片就繼續用,還是連導航欄都可能自定義了呢。
這里舉個例子,我們項目中用到了很多H5與Native的通信,于是就抽象了一個CEBaseWebViewController專門用來管理JS的注冊與移除,以及基礎Cookie設置。
網絡數據層
我們現在采用的是MVVM模式,ViewModel的分層可以讓ViewController中的數據交互都通過ViewModel來進行,ViewController與數據獲取已經完全隔離。
另外我封裝了一層網絡層,用于對接服務端接口,進一步將ViewModel的網絡依賴抽離出來。
// ViewController @interface CEMyWalletViewController : CEBaseViewController@property (strong, nonatomic) CEMyWalletViewModel *viewModel;@end// ViewModel @interface CEMyWalletViewModel : NSObject@property (assign, nonatomic) NSInteger currentPageIndex;@property (assign, nonatomic) CEWalletBillFilterType filterType;@property (strong, nonatomic) NSArray <CEWalletBillInfo *> *billList;@property (strong, nonatomic) CEWallet *myWallet;- (void)getMyWalletInfo:(BOOL)HUDVisible completion:(void(^)(BOOL success))completion;- (void)getWalletShortBillInfoList:(void(^)(BOOL success))completion;- (void)getWalletBillInfoList:(void(^)(BOOL success, BOOL hasMoreContent))completion;@end// Network @interface CEWalletNetworking : NSObject+ (void)getMyWalletDetail:(CENetworkingOption *)option completion:(CENetworkingCompletionBlock)completionBlock;+ (void)getWalletShortBillList:(CENetworkingOption *)option completion:(CENetworkingCompletionBlock)completionBlock;+ (void)getWalletBillListByPageNum:(NSInteger)pageNum billType:(CEWalletBillFilterType)billType option:(CENetworkingOption *)option completion:(CENetworkingCompletionBlock)completionBlock@end 復制代碼數據傳輸路徑
Networking/Database -> ViewModel -> ViewController
用接口的形式將數據提供給ViewModel,ViewModel來維護ViewController的數據,ViewController只需要維護View的顯示邏輯即可。
這樣不論是服務端接口變更,還是業務邏輯變更,都不會影響到ViewController。
這里可以抽象的組件主要是在Networking和Database這一層,比如我在Networking對AFNetworking進行了二次封裝,根據業務模塊進行劃分,方便業務使用。同樣,Database我們用的是CoreData,也對其進行了二次封裝。
ViewController的路由
方案選擇
原先開發的時候,是為每一個頁面都做了Category,作為路由邏輯的封裝。缺點就是,比如像入口比較多的首頁,就需要import多個Category。
學習了下網上流行的URLRouter,Protocol-Class和Target-Action方案,最后參考了Target-Action方案(傳送門:CTMediator)的思路。
主要考慮到在后期會考慮升級成路由表,在Target-Action的調度者中加入Url方案也比較容易,參數解析已經完成,不需要重復修改。
實現方案
首先是將跳轉邏輯統一管理起來,于是就又過了GHRouter。
GHRouter的主要作用是在運行時,請求頁面的消息通過反射的形式傳遞到正確的RouteMap上,從而執行正確的跳轉。
NS_ASSUME_NONNULL_BEGIN @interface GHRouter : NSObject/**用于檢測用于跳轉的Url是否為特定Url,默認不檢測*/ @property (nonatomic, strong) NSString *openUrlScheme; /**targetClass 實例緩存*/ @property (nonatomic, strong) NSMapTable *targetCache; /**默認緩存30個target,超過閾值后,會隨機移除一半。*/ @property (nonatomic, assign) NSInteger maxCacheTargetCount;/**默認檢測targetClassName是否以“RouteMap”結尾,賦值為nil可以關閉檢測。*/ @property (nonatomic, strong) NSString *targetClassNameSuffix;/**默認檢測selectorName是否以“routerTo”開頭,賦值為nil可以關閉檢測。*/ @property (nonatomic, strong) NSString *selectorNamePrefix;+ (instancetype)sharedInstance; /**通過URL跳轉指定頁面例如:MyProject://TargetClassName/SelectorName:?params1="phone"¶ms2="name"或MyProject://TargetClassName/SelectorName?params1="phone"¶ms2="name"SelectorName后面可以不帶冒號,會自動添加。@param url 傳入的URL@param validate 自定義校驗過程,傳入nil,則表示不做自定義校驗@return 返回值*/ - (id)performByUrl:(NSURL *)url validate:(BOOL(^)(NSURL *url))validate; /**例如:在路由Class中創建以下方法,用于跳轉。為了規范用法,第一位參數必須傳入NSDIctionary類型的對象。- (UIViewController *)routerToViewController:(NSDictionary *)params;- (void)routerToViewController:(NSDictionary *)params;@param targetClassName 路由Class名稱@param selectorName 調用的路由方法@param params 路由參數@return 返回值*/ - (id)performTargetClassName:(NSString *)targetClassName selectorName:(NSString *)selectorName params:( NSDictionary *__nullable)params;- (void)removeTargetCacheByClassName:(NSString *)className; - (void)cleanupTargetCache;@endNS_ASSUME_NONNULL_END復制代碼 @implementation GHRouter+ (instancetype)sharedInstance {static dispatch_once_t onceToken;static id sharedInstance = nil;dispatch_once(&onceToken, ^{sharedInstance = [[self alloc] init];});return sharedInstance; } - (instancetype)init {self = [super init];if (self) {[self setup];}return self; }- (void)dealloc {[[NSNotificationCenter defaultCenter] removeObserver:self]; }- (void)setup {_targetCache = [NSMapTable strongToStrongObjectsMapTable];_maxCacheTargetCount = 30;_selectorNamePrefix = @"routeTo";_targetClassNameSuffix = @"RouteMap";_openUrlScheme = nil;[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cleanupTargetCache) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; }- (id)performByUrl:(NSURL *)url validate:(BOOL(^)(NSURL *url))validate {if (_openUrlScheme.length != 0) {if (![url.scheme isEqualToString:_openUrlScheme]) {return [NSNull null];};}NSString *scheme = url.scheme;if (scheme.length == 0) { NSLog(@"ERROR: %s url.scheme is nil",__FUNCTION__); return [NSNull null];}NSString *targetClassName = url.host;if (targetClassName.length == 0) { NSLog(@"ERROR: %s url.host is nil",__FUNCTION__); return [NSNull null];}NSString *path = url.path;if (path.length == 0) { NSLog(@"ERROR: %s url.path is nil",__FUNCTION__); return [NSNull null];}if (validate) {if (!validate(url)) {return [NSNull null];};}NSMutableString *selectorName = [NSMutableString stringWithString:path];if ([selectorName hasPrefix:@"/"]) {[selectorName deleteCharactersInRange:NSMakeRange(0, 1)];}if (![selectorName hasSuffix:@":"]) {[selectorName stringByAppendingString:@":"];}NSDictionary *params = [self queryDictionary:url];return [self performTargetClassName:targetClassName selectorName:selectorName params:params]; }- (id)performTargetClassName:(NSString *)targetClassName selectorName:(NSString *)selectorName params:(NSDictionary *)params {NSAssert(targetClassName.length != 0, @"ERROR: %s \n targetClassName is nil",__FUNCTION__);NSAssert(selectorName.length != 0, @"ERROR: %s \n selectorName is nil",__FUNCTION__);NSAssert([selectorName hasSuffix:@":"], @"ERROR: %s \n selectorName (%@) must have params, such as \"routeToA:\"", __FUNCTION__, selectorName);if (_targetClassNameSuffix.length != 0) {NSAssert([targetClassName hasSuffix:_targetClassNameSuffix], @"ERROR: %s targetClassName must has suffix by \"%@\"",__FUNCTION__,_targetClassNameSuffix);}if (_selectorNamePrefix.length != 0) {NSAssert([selectorName hasPrefix:_selectorNamePrefix], @"ERROR: %s selectorName must has Prefix by \"%@\"",__FUNCTION__,_selectorNamePrefix);}Class targetClass = NSClassFromString(targetClassName);if (!targetClass) { NSLog(@"ERROR: %s targetClass can't found by targetClassName:\"%@\"",__FUNCTION__, targetClassName); return [NSNull null];}id target = [_targetCache objectForKey:targetClassName];if (!target) {target = [[targetClass alloc] init];}SEL selector = NSSelectorFromString(selectorName);if (![target respondsToSelector:selector]) { NSLog(@"ERROR:%s targetClassName:\"%@\" can't found selectorName:\"%@\"", __FUNCTION__, targetClassName, selectorName); return [NSNull null];} return [self performTarget:target selector:selector params:params]; }- (id)performTarget:(id)target selector:(SEL)selector params:(NSDictionary *)params {NSMethodSignature *method = [target methodSignatureForSelector:selector];if (!method) {return nil;}const char *returnType = [method methodReturnType];//返回值如果非對象類型,會報EXC_BAD_ACCESSif (strcmp(returnType, @encode(BOOL)) == 0) {NSInvocation *invocation = [self invocationByMethod:method target:target selector:selector params:params];[invocation invoke];BOOL *result = malloc(method.methodReturnLength);[invocation getReturnValue:result];NSNumber *returnObj = @(*result);free(result);return returnObj;} else if (strcmp(returnType, @encode(void)) == 0) {NSInvocation *invocation = [self invocationByMethod:method target:target selector:selector params:params];[invocation invoke];return [NSNull null];} else if (strcmp(returnType, @encode(unsigned int)) == 0|| strcmp(returnType, @encode(NSUInteger)) == 0) {NSInvocation *invocation = [self invocationByMethod:method target:target selector:selector params:params];[invocation invoke];NSUInteger *result = malloc(method.methodReturnLength);[invocation getReturnValue:result];NSNumber *returnObj = @(*result);free(result);return returnObj;} else if (strcmp(returnType, @encode(double)) == 0|| strcmp(returnType, @encode(float)) == 0|| strcmp(returnType, @encode(CGFloat)) == 0) {NSInvocation *invocation = [self invocationByMethod:method target:target selector:selector params:params];[invocation invoke];CGFloat *result = malloc(method.methodReturnLength);[invocation getReturnValue:result];NSNumber *returnObj = @(*result);free(result);return returnObj;} else if (strcmp(returnType, @encode(int)) == 0|| strcmp(returnType, @encode(NSInteger)) == 0) {NSInvocation *invocation = [self invocationByMethod:method target:target selector:selector params:params];[invocation invoke];NSInteger *result = malloc(method.methodReturnLength);[invocation getReturnValue:result];NSNumber *returnObj = @(*result);free(result);return returnObj;} return [target performSelector:selector withObject:params]; }- (NSInvocation *)invocationByMethod:(NSMethodSignature *)method target:(id)target selector:(SEL)selector params:(NSDictionary *)params {NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:method];[invocation setTarget:target];[invocation setSelector:selector];if (method.numberOfArguments > 2 && params) {[invocation setArgument:¶ms atIndex:2];}return invocation; }- (void)addTargetToCache:(id)target targetClassName:(NSString *)targetClassName { // 當緩存數量達到上限的時候,會隨機刪除一半的緩存if (_targetCache.count > _maxCacheTargetCount) {while (_targetCache.count > _maxCacheTargetCount/2) {[_targetCache removeObjectForKey:_targetCache.keyEnumerator.nextObject];}}[_targetCache setObject:target forKey:targetClassName]; }- (void)removeTargetCacheByClassName:(NSString *)className {[_targetCache removeObjectForKey:className]; }- (void)cleanupTargetCache {[_targetCache removeAllObjects]; }- (NSDictionary *)queryDictionary:(NSURL *)url {NSMutableDictionary *params = [[NSMutableDictionary alloc] init];NSString *urlString = [url query];for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {NSArray *elts = [param componentsSeparatedByString:@"="];if ([elts count] < 2) {continue;}[params setObject:[elts lastObject] forKey:[elts firstObject]];}return params; }@end復制代碼總結下Router通信流程
本地組件通信
遠程通信
這里舉個例子:比如有一個EditCompanyInfoViewController,首先要為EditInfoRouteMap,用于解析跳轉參數。這里要注意的是,由于參數是包裝在Dictionary中的,所以在route方法上請加上參數注釋,方便后期維護。
// .h @interface CEEditInfoRouteMap : NSObject/**跳轉公司信息編輯頁面@param params @{@"completion":void (^completion)(BOOL success, UIViewController *vc)}*/ - (void)routeToEditCompanyInfo:(NSDictionary *)params;@end// .m @implementation CEEditInfoRouteMap- (void)routeToEditCompanyInfo:(NSDictionary *)params {void (^completion)(BOOL success, UIViewController *vc) = params[@"completion"];CEEditCompanyInfoViewController *vc = [[CEEditCompanyInfoViewController alloc] init];[vc.viewModel getCompanyInfo:^(BOOL success) {completion(success,vc);}]; }@end復制代碼再者為CERouter創建一個Category,用于管理路由構造。
// .h @interface GHRouter (EditInfo)- (void)routeToEditCompanyInfo:(void(^)(BOOL success, UIViewController *vc))completion;@end// .m @implementation GHRouter (EditInfo)- (void)routeToEditCompanyInfo:(void(^)(BOOL success, UIViewController *vc))completion {Router(@"CEEditInfoRouteMap", @"routeToEditCompanyInfo:", @{@"completion":completion}); }@end復制代碼最終調用
- (void)editCompanyInfo {[[GHRouter sharedInstance] routeToEditCompanyInfo:^(BOOL success, UIViewController * _Nonnull vc) {[self.navigationController pushViewController:vc animated:YES];}]; } 復制代碼到這一步調用者依賴Router,Router通過NSInvocation與CEEditInfoRouteMap通信,CEEditInfoRouteMap依賴CEEditCompanyInfoViewController。
Router成為了單獨的組件,沒有依賴。
參考資料
iOS 組件化之路由設計思路分析
iOS開發——組件化及去Mode化方案
轉載于:https://juejin.im/post/5c877fd76fb9a049fa109baa
總結
以上是生活随笔為你收集整理的iOS项目组件化历程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cURL在Web渗透测试中的应用
- 下一篇: RocketMQ 事务消息