iOS項目架構

iOS項目架構

做了幾個App,發現很多時候,App的基本框架都是一樣的,如何組織架構,讓項目更容易開發和維護,減少耦合,成了不變的主題。
下面的嘮叨呢,是我基于最近做的一個App,在架構設計這方面的一些思考和實踐。
本文開發語言為Objective-C


問題的拋出

App常見設計

如上圖所示,大多數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都實現所需方法了)
首先看一張圖:

AppDelegate廣播

在上圖中,我利用AppMulticastDelegateAppDelegate的方法調用進行轉發給其他幾個子模塊,達到了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>協議,而是靠runtimeforwardInvocation(消息重定向)實現的消息轉發。
它的.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的消息轉發機制可以實現很多功能,比如多重代理,多繼承等, 這里推薦幾個不錯的文章:

繼續上面的話題,通過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消息轉發套路可以改成下圖這種方案:
AppDelegate廣播

不過此時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主要幾個控制器的生命周期,如下圖:

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類里面。
AppControllerListenerAppDelegate進行初始化之后,(注意這里可以選擇strongAppDelegate,或者直接將AppControllerListener單例化),就可以在AppControllerListener開心的處理之前我們說到的首頁問題,結合上面提到的AppLaunchInTool注意在.m文件方法體里,進行封裝和模塊化,讓代碼更加整潔。

這里多說一句就是,第三方庫并不是非要在application:didFinishLaunchingWithOptions:里面初始化,也不是非要在主線程初始化,根據所用第三方庫,在合適的時間和地方進行初始化,能加快App啟動速度。有些第三庫是有要求的,比如微信SDK就要求在主線程registerApp。有些第三方庫初始化時需要傳遞application:didFinishLaunchingWithOptions:方法的參數launchOptions,大可在AppDelegatelaunchOptions進行strong屬性化,以便后面傳遞使用即可。

三.App各種彈框

App彈框疊加,對于產品來說是個偽命題,因為好的產品設計會避免這種情況的發生,但是對于程序員來說,卻是不可避免會發生的,比如在網絡不好的情況下,快速切換頁面,就可能造成彈框的重疊。(這里說的彈框,不是那種添加到View上的吐司Toast提示。)。今天我打開簡書App就遇到了一堆彈框,并且出現了關閉彈框之后,黑色蒙層并沒有一起關閉的bug,如下圖,可以看到紅包彈框+通知權限彈框+新功能引導頁三個一起疊加顯示了:

簡書App彈框疊加bug

當有多個彈框同時彈出時,有以下常見的處理方式:

  • 1.依次彈出,新的彈框會讓之前的關閉,當處理完最上層彈框后,再彈出下面的彈框。這種做法最常見的就是蘋果App的權限請求彈框,當第一次打開App時,如果沒有處理好,就會瞬間彈出多個權限彈框,(通知/網絡/定位等).

  • 2.疊加顯示,這個也跟彈框的實現方式有關系,彈框無非是使用a.控制器,b.UIView,c.UIWindow三種方式,在疊加顯示時,需要處理好關閉,反正只要產品能接受,(不能說成是App的bug),不過這里不知道大家有沒有遇到過優先級問題,比如App有個強制更新App的彈框,當它彈出時,就必須不能被其他操作遮擋。

UIWindow有一個屬性windowLevelwindowLevel的大小決定了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項目的設計模式有很多(MVVMMVCMVP等),但是在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開發時,每個模塊或者其子模塊,一般都是三個文件夾,ViewModelController,有時加一個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。
我是小侯爺。
在帝都艱苦奮斗,白天是上班族,晚上是知識服務工作者。
如果讀完覺得有收獲的話,記得關注和點贊哦。
非要打賞的話,我也是不會拒絕的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。