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是两个分类,二者同时用到一个方法,此时就是无解的。

接下来想到通过runtime或者AOP拦截监听所有AppDelegate的方法,再分发给子模块,但是发现一个瑕疵就是,AppDelegate.m里面必须实现所有协议,不然根本获取不到对应的方法,何来监听?对此,网上也有类似方案在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 

- (void)addDelegate:(id)delegate;
- (void)removeDelegate:(id)delegate;
- (void)removeAllDelegates;

@end

让它实现协议是为了在AppDelegate.m里面方便直接hook调用的,(不然只能在AppDelegate.m使用respondsToSelector:@selector(),但是这样无法传递多个参数),不过它并不需要实现协议,而是靠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的消息转发机制可以实现很多功能,比如多重代理,多继承等, 这里推荐几个不错的文章:

  • Leesim的iOS Runtime 消息转发机制原理和实际用途
  • bang大神的JSPatch实现原理详解
  • 腾讯某大神博客,里面有源码级别的详细分析 点我

继续上面的话题,通过AppMulticastDelegate将方法调用转发给所有子模块,子模块只需要实现自己需要的协议方法,进行业务处理即可,代码纯粹且单一,有利于维护。比如单独处理推送的AppJPUSHDelegate

@interface AppJPUSHDelegate : UIResponder 
@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,其他的就按照上面的方法去编码即可。

有一点需要注意,协议有些方法可能需要回调,在使用上面的消息转发时,只需要写一次即可,比如下面这个方法:

- (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 

// ---------------------------登录---------------------------
/// 登录控制器ViewDidLoad (比如请求权限,隐私协议弹框)
- (void)appLoginControllerViewDidLoad;

// ---------------------------架子---------------------------
/// App框架的TabBarController (比如初始化分享第三方库,因为它被调用,意味着一定是登录了)
- (void)appTabBarControllerViewDidLoad;

// ---------------------------主要---------------------------
/// 首页ViewDidLoad
- (void)appHomeControllerViewDidLoad;
/// 用上面这种,还是下面这种,看App业务复杂度了
- (void)appController:(id)controller lifeCycle:(AppControllerLife)lifeCycle;

@end

// -----------------------------------------------------------
// -------------------------separator-------------------------
// -----------------------------------------------------------

@interface AppControllerListener : NSObject 

@end

看了代码之后,可能您会问为什么还写个AppControllerLifeCycleDelegate,干嘛不直接把方法定义到AppControllerListener类里面,嘿嘿,其实就是仿照设计的,就这个功能和目的而言,的确可以定义到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,在系统库里的类NSNotification.h里,已经帮我们定义了类型别名:

typedef NSString *NSNotificationName NS_EXTENSIBLE_STRING_ENUM;

所以别再直接NSString去定义通知名,仿照的命名规范,应该是位置+事件+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的,不过这个宏是定义在里面的,类似的,我们可以使用系统库里的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__这里不做过多解读,想了解的话推荐阅读下面几篇文章:

  • iOS attribute那点小事
  • iOS中常用的Attribute
  • OC中的 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。
我是小侯爷。
在帝都艰苦奋斗,白天是上班族,晚上是知识服务工作者。
如果读完觉得有收获的话,记得关注和点赞哦。
非要打赏的话,我也是不会拒绝的。

你可能感兴趣的:(iOS项目架构)