日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

iOS项目组件化历程

發布時間:2024/1/17 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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上,從而執行正確的跳轉。

#import <Foundation/Foundation.h>#define Router(targetClsName,selName,paramsDic) ([[GHRouter sharedInstance] performTargetClassName:(targetClsName) selectorName:(selName) params:(paramsDic)])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"&params2="name"或MyProject://TargetClassName/SelectorName?params1="phone"&params2="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復制代碼#import <UIKit/UIKit.h> #import "GHRouter.h"@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) { #ifdef DEBUGNSLog(@"ERROR: %s url.scheme is nil",__FUNCTION__); #endifreturn [NSNull null];}NSString *targetClassName = url.host;if (targetClassName.length == 0) { #ifdef DEBUGNSLog(@"ERROR: %s url.host is nil",__FUNCTION__); #endifreturn [NSNull null];}NSString *path = url.path;if (path.length == 0) { #ifdef DEBUGNSLog(@"ERROR: %s url.path is nil",__FUNCTION__); #endifreturn [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) { #ifdef DEBUGNSLog(@"ERROR: %s targetClass can't found by targetClassName:\"%@\"",__FUNCTION__, targetClassName); #endifreturn [NSNull null];}id target = [_targetCache objectForKey:targetClassName];if (!target) {target = [[targetClass alloc] init];}SEL selector = NSSelectorFromString(selectorName);if (![target respondsToSelector:selector]) { #ifdef DEBUGNSLog(@"ERROR:%s targetClassName:\"%@\" can't found selectorName:\"%@\"", __FUNCTION__, targetClassName, selectorName); #endifreturn [NSNull null];}#pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks"return [self performTarget:target selector:selector params:params]; #pragma clang diagnostic pop }#pragma mark- Private Method- (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;} #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks"return [target performSelector:selector withObject:params]; #pragma clang diagnostic pop }- (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:&params atIndex:2];}return invocation; }#pragma mark Cache- (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]; }#pragma mark- Private Method- (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通信流程

本地組件通信

  • Router收到請求,通過TargetClassName與SelectorName來尋找對應的Class與Selector,期間會校驗TargetClassName是否以“RouteMap”結尾,SelectorName是否以“routeTo”,以規范和區分路由類。
  • selector可以被響應后,會創建對應Class的對象(不用靜態方法是因為靜態方法在類加載的時候就會被初始化到內存中,而成員方法在實例初始化時才會被加載到內存中,使用靜態方法會影響到啟動速度),并加入緩存,通過methodSignatureForSelector獲取對應的NSMethodSignature
  • 構建NSInvocation并加入Params。
  • 觸發NSInvocation,并獲取返回值。對返回值進行判斷,非對象類型的返回值包裝成NSNumber,無返回值類型返回nil,以防止在獲取返回值時出現Crash,或者類型出錯。
  • 當緩存的Target達到閾值時,會被釋放掉一半的緩存,當收到內存警告時,會釋放掉所有的緩存。
  • 遠程通信

  • Router收到Url,先校驗Scheme,再從Url中解析出TargetClassName、SelectorName和Params。
  • 進行自定義驗證。
  • 進入本地組件通信流程。
  • 這里舉個例子:比如有一個EditCompanyInfoViewController,首先要為EditInfoRouteMap,用于解析跳轉參數。這里要注意的是,由于參數是包裝在Dictionary中的,所以在route方法上請加上參數注釋,方便后期維護。

    // .h @interface CEEditInfoRouteMap : NSObject/**跳轉公司信息編輯頁面@param params @{@"completion":void (^completion)(BOOL success, UIViewController *vc)}*/ - (void)routeToEditCompanyInfo:(NSDictionary *)params;@end// .m #import "CEEditInfoRouteMap.h" #import "CEEditCompanyInfoViewController.h"@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 #import "GHRouter.h"@interface GHRouter (EditInfo)- (void)routeToEditCompanyInfo:(void(^)(BOOL success, UIViewController *vc))completion;@end// .m #import "GHRouter+EditInfo.h"@implementation GHRouter (EditInfo)- (void)routeToEditCompanyInfo:(void(^)(BOOL success, UIViewController *vc))completion {Router(@"CEEditInfoRouteMap", @"routeToEditCompanyInfo:", @{@"completion":completion}); }@end復制代碼

    最終調用

    #import "GHRouter+EditInfo.h"- (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项目组件化历程的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。