iOS項目架構
做了幾個App,發現很多時候,App的基本框架都是一樣的,如何組織架構,讓項目更容易開發和維護,減少耦合,成了不變的主題。
下面的嘮叨呢,是我基于最近做的一個App,在架構設計這方面的一些思考和實踐。
本文開發語言為Objective-C
問題的拋出
如上圖所示,大多數App是這樣的架構模式:登錄注冊之后,采用UITabBarController + UINavigationController + UIViewController
進行組織頁面,看起來挺簡單的,可如果里面包含了推送、IM、定位、分享、支付、各種彈框等,在沒有好的規劃編碼下,就會變得越來越難以維護,常見的問題集中在以下幾個方面:
一: AppDelegate
AppDelegate.m 代碼越來越多,并且有點混亂,里面主要處理了以下邏輯:
1.推送邏輯
2.IM聊天(IM推送/IM信息處理)
3.分享回調處理
4.支付回調處理
5.scheme URL
6.3D Touch處理
7.初始化和加載一些資源
8.前后臺切換時持久化資源
二: 首頁
這個首頁就是打開App后第一個呈現的頁面,這里為什么說首頁?因為打開App,進入首頁,會進行很多請求和處理,比如下面這些問題,雖然一些請求或者邏輯可以放在其他地方,但是放在哪里更加合理?而且,如果App有開屏廣告,還需要等廣告關閉了再請求或者彈框(當然這里取決你的廣告頁是用的View
還是Controller
).
1.請求是否需要彈更新版本的提示框
2.請求是否需要彈領取紅包彈框
3.鏈接IM,并檢查Token
4.請求個人信息接口,并更新本地個人信息接口
5.開始更新定位(如果有權限)
6.是否彈請求權限的提示框
三: App各種彈框
這里說的彈框,不是Toast,而是UI小姐姐設計的各種業務彈框或者UIAlertController
,當App里面的彈框比較多時,如果同時有多個彈框請求,如何處理?特別是網絡不好的時候,很容易造成頁面疊加錯亂。(別忘了,可能還有新功能引導View)
四: 通知
對于通知NSNotificationCenter
,當業務邏輯讓你不得不使用時,如何有效的管理NSNotificationName
,在剛剛做iOS的時候,直接使用的字符串,造成后續迭代過程中,某塊業務都刪了,其他地方還在接收這些神奇字符串的通知。
五: 接口API
一個App有很多接口API,這些接口API如果直接寫在方法里面,顯然十分不好管理和查找,也不利于版本迭代控制,那么把這API統一寫在一個地方,該如何定義,如何進行版本控制(廢棄/從哪個版本可用)等。
六: MVC怎么說
網上有很多iOS設計模式的講解,不同的設計模式都是為了解決某些特殊問題,比如解耦。在iOS開發中其實用的最多還是MVC,但是有些代碼,寫在M-V-C三者哪里更合適?比如富文本的組裝、根據多個枚舉獲得一個值、拼裝和格式化一個時間的顯示等。
我的解決方案↓↓
一.AppDelegate
對于AppDelegate
而言,由于其職責很多,造成很多不同功能的代碼都在一起,所以我們的目標是解耦,
關于解耦 AppDelegate ,做了很多研究,網上也有很多方案,我最初的設想是利用分類Category
, 分類無疑能減少AppDelegate
里面的代碼,并且不需要在AppDelegate.m
里面再寫一遍方法的實現,但是Category
也有一個致命的問題就是有多個分類,同時實現一個方法時,只會調用其中一個。假如推送和IM是兩個分類,二者同時用到一個<UIApplicationDelegate>
方法,此時就是無解的。
接下來想到通過runtime
或者AOP
攔截監聽所有AppDelegate
的方法,再分發給子模塊,但是發現一個瑕疵就是,AppDelegate.m
里面必須實現所有<UIApplicationDelegate>
協議,不然根本獲取不到對應的方法,何來監聽?對此,網上也有類似方案在AppDelegate.m
實現完所有的協議方法,然后hook每個方法進行消息的轉發處理。不過我這里的方法跟別人的也有些不一樣的地方,大體思路是AppDelegate.m
都實現所需方法,由一個模塊管理者進行方法的轉發處理,所有的子模塊,只需要注冊模塊管理者,就能得到回調。(PS: iOS 13之后,其實也不需要AppDelegate.m
都實現所需方法了)
首先看一張圖:
在上圖中,我利用
AppMulticastDelegate
將AppDelegate
的方法調用進行轉發給其他幾個子模塊,達到了AppDelegate
代碼的解耦,功能的單一原則。此時,在
AppDelegate
里面代碼就比較純粹了,僅是為了給AppMulticastDelegate
提供hook
,系統對AppDelegate
的方法調用,都會轉發給所有AppXXDelegate
類,AppDelegate.m
代碼精簡為如下:
@implementation AppDelegate
- (instancetype)init {
if (self = [super init]) {
self.multicast = [[AppMulticastDelegate alloc] init];
[self.multicast addDelegate:[AppJPUSHDelegate new]]; // 極光推送
[self.multicast addDelegate:[AppIMDelegate new]]; // 環信IM+IM推送
[self.multicast addDelegate:[AppPayDelegate new]]; // 支付/分享回調
}
return self;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
self.window.rootViewController = [[AppMainController alloc] init];
[self.window makeKeyAndVisible];
return YES;
}
#pragma mark - 推送相關
- (void)applicationDidEnterBackground:(UIApplication *)application {
[self.multicast applicationDidEnterBackground:application];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
[self.multicast applicationWillEnterForeground:application];
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[self.multicast application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
// more....
@end
再說圖中的AppMulticastDelegate
,它的功能就是轉發所有AppDelegate
方法調用,所以它叫廣播、路由、監聽、轉發等都好,就看你怎么理解了。那它是怎么實現的呢?它的.h
代碼如下(簡版):
@interface AppMulticastDelegate : NSObject <UIApplicationDelegate>
- (void)addDelegate:(id)delegate;
- (void)removeDelegate:(id)delegate;
- (void)removeAllDelegates;
@end
讓它實現<UIApplicationDelegate>
協議是為了在AppDelegate.m
里面方便直接hook調用的,(不然只能在AppDelegate.m
使用respondsToSelector:@selector()
,但是這樣無法傳遞多個參數),不過它并不需要實現<UIApplicationDelegate>
協議,而是靠runtime
的forwardInvocation
(消息重定向)實現的消息轉發。
它的.m
核心思路如下:
@interface AppMulticastDelegate ()
@property (nonatomic ,strong) NSMutableArray *delegateArray;
@end
@implementation AppMulticastDelegate
// MARK: - Public
- (void)addDelegate:(id)delegate {
....
}
- (void)removeDelegate:(id)delegate {
....
}
- (void)removeAllDelegates {
....
}
// MARK: - Forward
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
// 遍歷self.delegateArray, 查找方法簽名
....
}
- (void)forwardInvocation:(NSInvocation *)origInvocation {
SEL selector = [origInvocation selector];
// 遍歷self.delegateArray, 對所有實現了selector的delegate進行消息轉發
....
}
@end
正如你所看到的AppMulticastDelegate
并沒有多少行代碼,由于forwardInvocation
消息重定向的實現原理網上有很多大神寫過,這里就不細說了,僅僅提供思路(這里的代碼我沒有粘完,如果你真的需要,可以留言一起研究),利用Runtime的消息轉發機制可以實現很多功能,比如多重代理,多繼承等, 這里推薦幾個不錯的文章:
- 簡書Leesim的iOS Runtime 消息轉發機制原理和實際用途
- bang大神的JSPatch實現原理詳解
- 騰訊某大神博客,里面有源碼級別的詳細分析 點我
繼續上面的話題,通過AppMulticastDelegate
將方法調用轉發給所有子模塊,子模塊只需要實現自己需要的<UIApplicationDelegate>
協議方法,進行業務處理即可,代碼純粹且單一,有利于維護。比如單獨處理推送的AppJPUSHDelegate
:
@interface AppJPUSHDelegate : UIResponder <UIApplicationDelegate>
@end
@implementation AppJPUSHDelegate
- (void)applicationDidEnterBackground:(UIApplication *)application {
// TODO...
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// TODO...
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
// 極光推送-注冊APNs, 上報DeviceToken
[JPUSHService registerDeviceToken:deviceToken];
}
- (void)application:(UIApplication *)application
didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
NSLog(@"did Fail To Register For Remote Notifications With Error: %@", error);
}
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
// iOS10以下,處理本地通知
}
// 配合JPUSHRegisterDelegate處理 APNs
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
// iOS7 -> iOS9, 處理 APNs 通知回調方法, 收到通知:userInfo
// 處理收到的 APNs 消息
[JPUSHService handleRemoteNotification:userInfo];
// completionHandler(UIBackgroundFetchResultNewData); 這里不必再調用
}
@end
是不是看起來純潔很多?
不知道你發現,這里我沒寫初始化這些第三方庫(eg.極光推送)的代碼,是因為這里有個問題是,對第三方庫的封裝,這里強烈建議使用第三方庫時,再封裝一次,好處多多,比如方便替換第三方庫,第三方庫更新時,也不用整個項目去修復。當你把第三方都封裝一層時,AppDelegate
此時初始化第三方就如下這樣:
#pragma mark - 第三方設置
- (void)otherLibarayWithOptions:(NSDictionary *)launchOptions {
// 1.支付(微信/支付寶/銀聯)
[[HWPayManager sharedPayManager] pay_registerApp];
// 2.App推送
[HWJPUSHConfig configWithOptions:launchOptions delegate:self];
// 3.融云IM
[HWRCIMConfig configWithOptions:launchOptions];
// 4.App統計
[HWANALYTICSService config];
// 5.App分享
[HWJShareConfig config];
}
此時按照AppMulticastDelegate
轉發消息的思想,這些代碼就可以寫到各自模塊的application:didFinishLaunchingWithOptions:
方法里調用即可了。
在第三方庫初始化的這里,還有一點,值得思考的是,初始化的時機,比如用戶未登錄的時候,打開App就初始化了分享模塊,是否有必要?那如果是登錄后再初始化,這些代碼放在哪里合適?這里留個小坑,在下面首頁那里給出我的做法。
????iOS 13之后,蘋果意識到AppDelegate
干的事情太多,不利于維護,加上手機屏幕越來越大,App可能有分屏的情況,造成多個Scene
,所以iOS 13之后,AppDelegate
的職責發現了改變:
- iOS13之前,
AppDelegate
的職責全權處理App生命周期和UI生命周期; - iOS13之后,
AppDelegate
的職責是:
1>處理 App 生命周期,2>新的Scene Session
生命周期UI的生命周期則交給新增的SceneDelegate
處理,UIWindow
也放在了SceneDelegate
里面進行管理.
所以對于iOS 13新建的項目,AppMulticastDelegate
消息轉發套路可以改成下圖這種方案:
不過此時AppMulticastDelegate
需要繼承UIResponder
,其他的就按照上面的方法去編碼即可。
有一點需要注意,<UIApplicationDelegate>
協議有些方法可能需要回調,在使用上面的消息轉發時,只需要寫一次即可,比如下面這個方法:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
// 1.轉發
[self.xxx application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
// 2.回調,不過只需要一次即可,子delegate不需要再執行此語句
completionHandler(UIBackgroundFetchResultNewData);
}
二.首頁
正如上面說到的,這里首頁的定義是打開App后的第一個頁面,由于業務和產品的需求,會在首頁執行很多邏輯,曾經我是直接寫在HomeControllerView
里面的,后面迭代次數多了,首頁功能代碼和這些代碼糅雜在一起,十分痛苦。
后來我進行優化分離,我建立一個單例類AppLaunchInTool
,此單例類會在HomeControllerView
里面進行第一次創建并處理所有跟首頁沒關系的代碼,如下:
@interface AppLaunchInTool : NSObject
/// 進行融云IM的Token檢查,沒有Token將請求Token并設置,
- (void)checkIM_Token;
/// App版本檢查
- (void)checkAppVersion;
/// 用戶是有紅包并彈框提示
- (void)haveRedPacket;
/// 用戶是否獲得了新勛章
- (void)haveNewMedal;
#pragma mark - 單例
+ (instancetype)sharedLaunchInTool;
@end
此時HomeControllerView
的確純潔了起來,AppLaunchInTool
也只需要在初始化時,自己調用這些獨立業務的方法即可,似乎很完美。但是產品某天突然說XX不放在首頁了,放在[我的]頁面去...,再回想起上面我們說的個別第三庫初始化時機問題(我曾經在AppLaunchInTool
里面加入初始化分享/推送等第三方庫,用于登錄后調用),慢慢的AppLaunchInTool
在其他很多地方開始主動被調用。感覺僅僅是對之前的代碼進行抽離封裝下,并沒有解決在首頁調用非首頁邏輯的本質問題。
????回看蘋果的設計:App的生命周期通過AppDelegate
回調得到,那我們就仿照這個思想,建立一個類,用來獲得App主要幾個控制器的生命周期,如下圖:
AppControllerListener
類監聽App主要控制器的生命周期,從而將跟控制器無關業務代碼進行剝離,在AppControllerListener
里面又分模塊的去調用處理,達到了代碼的解耦和純潔。
至于AppControllerListener
怎么監聽App主要控制器的生命周期,無非是a.控制器直接調用,b.通知,c.基類,這里我都使用過,建議是根據項目的復雜度來決定怎么做,比如a.控制器直接調用,就在LoginViewController
的幾個生命周期方法里直接調用即可,eg:[AppControllerListener appLoginControllerViewDidLoad];
。AppControllerListener
的代碼可以如下:
typedef enum : NSUInteger {
AppControllerLifeViewDidLoad,
AppControllerLifeViewWillAppear,
AppControllerLifeviewWillDisappear,
} AppControllerLife;
/// App主要控制器的生命周期(簡版)
@protocol AppControllerLifeCycleDelegate <NSObject>
// ---------------------------登錄---------------------------
/// 登錄控制器ViewDidLoad (比如請求權限,隱私協議彈框)
- (void)appLoginControllerViewDidLoad;
// ---------------------------架子---------------------------
/// App框架的TabBarController (比如初始化分享第三方庫,因為它被調用,意味著一定是登錄了)
- (void)appTabBarControllerViewDidLoad;
// ---------------------------主要---------------------------
/// 首頁ViewDidLoad
- (void)appHomeControllerViewDidLoad;
/// 用上面這種,還是下面這種,看App業務復雜度了
- (void)appController:(id)controller lifeCycle:(AppControllerLife)lifeCycle;
@end
// -----------------------------------------------------------
// -------------------------separator-------------------------
// -----------------------------------------------------------
@interface AppControllerListener : NSObject <AppControllerLifeCycleDelegate>
@end
看了代碼之后,可能您會問為什么還寫個AppControllerLifeCycleDelegate
,干嘛不直接把方法定義到AppControllerListener
類里面,嘿嘿,其實就是仿照<UIApplicationDelegate>
設計的,就這個功能和目的而言,的確可以定義到AppControllerListener
類里面。
將AppControllerListener
在AppDelegate
進行初始化之后,(注意這里可以選擇strong
到AppDelegate
,或者直接將AppControllerListener
單例化),就可以在AppControllerListener
開心的處理之前我們說到的首頁問題,結合上面提到的AppLaunchInTool
注意在.m
文件方法體里,進行封裝和模塊化,讓代碼更加整潔。
這里多說一句就是,第三方庫并不是非要在application:didFinishLaunchingWithOptions:
里面初始化,也不是非要在主線程初始化,根據所用第三方庫,在合適的時間和地方進行初始化,能加快App啟動速度。有些第三庫是有要求的,比如微信SDK就要求在主線程registerApp
。有些第三方庫初始化時需要傳遞application:didFinishLaunchingWithOptions:
方法的參數launchOptions
,大可在AppDelegate
對launchOptions
進行strong
屬性化,以便后面傳遞使用即可。
三.App各種彈框
App彈框疊加,對于產品來說是個偽命題,因為好的產品設計會避免這種情況的發生,但是對于程序員來說,卻是不可避免會發生的,比如在網絡不好的情況下,快速切換頁面,就可能造成彈框的重疊。(這里說的彈框,不是那種添加到View上的吐司Toast提示。)。今天我打開簡書App就遇到了一堆彈框,并且出現了關閉彈框之后,黑色蒙層并沒有一起關閉的bug,如下圖,可以看到紅包彈框+通知權限彈框+新功能引導頁
三個一起疊加顯示了:
當有多個彈框同時彈出時,有以下常見的處理方式:
1.依次彈出,新的彈框會讓之前的關閉,當處理完最上層彈框后,再彈出下面的彈框。這種做法最常見的就是蘋果App的權限請求彈框,當第一次打開App時,如果沒有處理好,就會瞬間彈出多個權限彈框,(通知/網絡/定位等).
2.疊加顯示,這個也跟彈框的實現方式有關系,彈框無非是使用a.控制器,b.
UIView
,c.UIWindow
三種方式,在疊加顯示時,需要處理好關閉,反正只要產品能接受,(不能說成是App的bug),不過這里不知道大家有沒有遇到過優先級問題,比如App有個強制更新App的彈框,當它彈出時,就必須不能被其他操作遮擋。
UIWindow
有一個屬性windowLevel
,windowLevel
的大小決定了UIWindow
顯示的層級位置。仿照這種思想,我在開發中設計了一個彈框隊列管理類,給所有彈框都賦值了alertLevel
屬性,alertLevel
高的,就會優先顯示,用戶關閉之后,就會顯示隊列里面的下一個,既不會出現疊加,也能讓所有的彈框都能按照預期呈現給用戶。不過在開發中,特別是多人開發,都需要統一使用彈框基類,利用彈框管理器進行彈框。
由于這塊跟項目需求很緊密,我沒有整理單獨的代碼,如果需要參考的,可以留言,我會整理下貼出來代碼。
四.通知
對于使用NSNotificationCenter
要嚴格要求不能直接使用字符串當NSNotificationName
,在系統庫<Foundation>
里的類NSNotification.h
里,已經幫我們定義了類型別名:
typedef NSString *NSNotificationName NS_EXTENSIBLE_STRING_ENUM;
所以別再直接NSString
去定義通知名,仿照<UIKit>
的命名規范,應該是位置+事件+Notification
來組成我們的通知名。通知名應該定義在發送通知的類.h里面,簡單說是: 誰發通知,誰定義通知名。例如在UIWindow.h
里面定義的幾個通知名:
UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeVisibleNotification;
UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeHiddenNotification;
UIKIT_EXTERN NSNotificationName const UIWindowDidBecomeKeyNotification;
UIKIT_EXTERN NSNotificationName const UIWindowDidResignKeyNotification;
UIKIT_EXTERN
這個修飾宏應該都知道,簡單說就是讓被修飾常量對外界是public的,不過這個宏是定義在<UIKit>
里面的,類似的,我們可以使用系統庫<Foundation>
里的FOUNDATION_EXTERN
。通知名稱是固定不可變的,并且不允許外界修改值,所以加上了const
關鍵字。
到此我們的通知名就可以這么寫:
// .h頭文件
FOUNDATION_EXTERN NSNotificationName const SPDebugShowNotification; // Debug顯示通知
FOUNDATION_EXTERN NSNotificationName const SPDebugHideNotification; // Debug隱藏通知
// .m文件
NSNotificationName const SPDebugShowNotification = @"DShow";
NSNotificationName const SPDebugHideNotification = @"DHide";
五.接口API
一個App有很多接口API,這些接口API該如何定義,如何進行版本控制(廢棄/從哪個版本可用)等。
首先,把接口直接寫在項目各個調用的地方,是不可取的,混亂不利于管理。其次把接口定義為宏也是不可取的,因為宏會讓編譯速度巨慢,也會沒有類型安全檢查。
首先,為了方便管理我們的API,我們需要在API文件的.h
頭文件里定義以下幾個宏:
/// 給NSString起個別名,看起來整齊劃一,高大上
typedef NSString *SPAPI;
/// 廢棄的版本,還能使用,并沒有移除,強烈建議不使用
#define SPAPI_DEPRECATED(D) __attribute__((deprecated(D)));
/// 移除的版本,不能再使用
#define SPAPI_UNAVAILABLE(D) __attribute__((unavailable(D)));
其中SP
前綴是項目名的簡寫,這個大家根據自己情況去定義即可,有了上面的準備,接下來就提供兩種API組織方式:
懶人式:(之所以叫懶人式是因為這個只有一個.h文件即可)
// ================================================================
// MARK: - 登錄
// ================================================================
/// 登錄
static SPAPI const api_login = @"employee/login";
/// 登錄 獲取驗證碼
static SPAPI const api_login_code = @"employee/getCode";
// ================================================================
// MARK: - 我的
// ================================================================
/// 我的頁面
static SPAPI const api_me_index = @"me/index";
也許你發現了,這個懶人式雖然用到了我們定義的SPAPI
,但是由于是靜態常量(static),所以無法進行版本管理,不過由于寫著簡單,只有一個.h文件,所以很小的項目,也可以考慮這種方式,。
標準式:(跟上面說的通知名那里是一樣的道理),標準式需要.h
和.m
一起寫。例如(.h
文件):
// ================================================================
// MARK: - 登錄
// ================================================================
/// 登錄
FOUNDATION_EXTERN SPAPI const api_login;
/// 登錄 獲取驗證碼
FOUNDATION_EXTERN SPAPI const api_login_code SPAPI_DEPRECATED("v3.2.0起不再使用");
// ================================================================
// MARK: - 我的
// ================================================================
/// 我的頁面
FOUNDATION_EXTERN SPAPI const api_me_index;
/// 我的積分數量
FOUNDATION_EXTERN SPAPI const api_me_score SPAPI_UNAVAILABLE("v1.2.5已作廢");
那么.m
文件就很簡單了:
// ================================================================
// MARK: - 登錄
// ================================================================
/// 登錄
SPAPI const api_login = @"employee/login";
/// 登錄 獲取驗證碼
SPAPI const api_login_code = @"employee/getCode";
// ================================================================
// MARK: - 我的
// ================================================================
/// 我的頁面
SPAPI const api_me_index = @"me/index";
/// 我的積分數量
SPAPI const api_me_score = @"me/getScore";
正如你所看到的,我用了自定義的SPAPI_DEPRECATED
進行API版本提示管理,你如果說API既然過期了或者廢棄了,干嘛不直接刪了,還留著,那么等你去解決老版本的bug問題時,你就知道用處了。
這里想再說的一點是API的命名問題,由于后臺人員可能是多人開發的,不一定規范,加上為了方便我們自己對API的管理和理解,在給API起名的時候,建議是api_模塊名_接口名
或者api_模塊名_子模塊名_接口名
的方式去命名。在上面的代碼中,我為了方便一眼看出這個API的含義,就沒把API按照常量的方式去全部大寫,如果你感覺不爽,可以定義成:FOUNDATION_EXTERN SPAPI const API_LOGIN
的形式。
自定義宏SPAPI_DEPRECATED
用的是__attribute__函數
,那么關于__attribute__
這里不做過多解讀,想了解的話推薦閱讀下面幾篇文章:
六.MVC怎么說
iOS項目的設計模式有很多(MVVM
、MVC
、MVP
等),但是在iOS開發中其實用的最多還是MVC,而iOS開發中的MVC用法幾乎是:V是創建View并布局,M是請求到的數據模型(或者為了方便顯示而創建的UIModel),C就是請求數據/處理業務,在合適的時機給V賦值,有時候還在C里面寫V的布局,這種開發模式對于中小App來說,效率還是比較快的。但是如果UI小姐姐設計比較潮,或者業務判斷比較多時,就可能有很多類似下面的代碼:
// 例子A
- (void)setupModel:(OrderModel *)model {
if (model.type == 1) {
self.typeLabel.text = @"未付款";
} else if (model.type == 2) {
self.typeLabel.text = @"已付款";
} else if (model.type == 3) {
self.typeLabel.text = @"已取消";
} else {
self.typeLabel.text = @"已完成";
}
// ...后續邏輯代碼...
}
// 例子B
- (void)societyName:(NSString *)name nickName:(NSString *)nickName {
NSString *s = [NSString stringWithFormat:@"%@ | 昵稱:%@",name,nickName];
NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:s];
attr.yy_font = UIFont systemFontOfSize:20 weight:(UIFontWeightMedium)];
attr.yy_color = HWColorEX(0x333333);
NSRange r = [s rangeOfString:[NSString stringWithFormat:@" | 昵稱:%@",nickName]];
font = [UIFont systemFontOfSize:12 weight:(UIFontWeightMedium)];
[attr yy_setFont:font range:r];
[attr yy_setColor:HWColorEX(0x4A5F2D) range:r];
self.nameLabel.attributedText = attr;
}
先不說這些代碼是在V、M或C里,它的確影響了主邏輯,當你后續維護過程中,想理清某塊的業務邏輯代碼時,往往會因為大量的if else
、富文本、格式化日期、枚舉判斷等次邏輯里頭疼不已,(這里的次邏輯,我把它的定義為跟為了格式化顯示/判斷傳參等的代碼),一般情況下,大多數同學會將這些代碼抽成方法,讓代碼整體更加整齊些,這里我提供一個思路:每個模塊增加一個類Tools,將大量次邏輯代碼扔到Tools里面,在MVC開發時,每個模塊或者其子模塊,一般都是三個文件夾,View
、Model
和Controller
,有時加一個Module
來放子頁面的MVC,那么新建的這個Tool類,可以新建一個Tool
文件夾,比如我們上面的代碼,利用Tool類之后就會是如下這樣:
// ========================Tool的定義========================
@interface SPOrderTool : NSObject
/**
根據后臺返回的type對應的類型,返回UI所需的字符串
@param 訂單的type類型,see: SPOrderModel.type
@return type對應的字符串描述
*/
+ (NSString *)typeStringWith:(NSInteger)type;
@end
// ========================用的時候========================
- (void)setupModel:(OrderModel *)model {
self.typeLabel.text = [SPOrderTool typeStringWith:model.type];
// ...后續邏輯代碼...
}
- (void)societyInfo:(SPSocietyModel *)model {
// 1.名字和昵稱的富文本顯示
self.nameLabel.attributedText = [SPSocietyTool name:model.name nickName:model.nickName];
// ...后續邏輯代碼...
}
看到這里,可能有疑問是:這樣跟抽成方法有啥區別?無法是一個在本類里面,一個在另外一個類里,而且這個Tool還得新建一個類!對于這個疑問:首先這些代碼的抽走,無疑減少了MVC各個類里面的代碼量,維護時更加清晰了,其次這個Tool的類方法,很可能不僅僅在V
里面用到了,也可能在C
里面用到了,它增加了代碼的復用性。最后一點注意就是Tool類并不是定義一次,它應該是每個模塊都有自己的Tool,子模塊也有自己的,一些子模塊很有可能用到上層模塊里面的Tool方法(比如傳遞模型時),這倒也說明了增加了代碼的復用性。
貼一段我寫的Tool:
// ========================例子A========================
@interface SocietyFormatTools : NSObject
/**
* 格式化時間 yyyy-MM-dd HH:mm:ss --> 剛剛/x分鐘前...
*/
+ (NSString *)formatTime:(NSString *)date;
/**
根據圖片類型,返回圖片類型字符串;比如 SDImageFormatPNG --> png
@param type SDWebImage 里的 SDImageFormat 枚舉值
*/
+ (NSString *)imageTypeName:(SDImageFormat)type;
/// 富文本,動態x條
+ (NSAttributedString *)societyCount:(NSInteger)count;
@end
// ========================例子B========================
/// 由于模塊內多處用到,故也使用Tool的方式
@interface SocietyRequestTools : NSObject
/**
* 收藏/取消收藏
@param msgInfoId 動態ID
@param type 0收藏 1取消收藏
*/
+ (void)Collect:(NSString *)msgInfoId type:(NSInteger)type success:(void (^ __nullable)(NSDictionary *JSON))success
failure:(void (^ __nullable)(NSError *error))failure;
// more....
@end
-- End ---
PS:最近我有跳槽的想法,有工作機會的老板,歡迎騷擾哦!北京呦!
END。
我是小侯爺。
在帝都艱苦奮斗,白天是上班族,晚上是知識服務工作者。
如果讀完覺得有收獲的話,記得關注和點贊哦。
非要打賞的話,我也是不會拒絕的。