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是两个分类,二者同时用到一个
方法,此时就是无解的。
接下来想到通过runtime
或者AOP
拦截监听所有AppDelegate
的方法,再分发给子模块,但是发现一个瑕疵就是,AppDelegate.m
里面必须实现所有
协议,不然根本获取不到对应的方法,何来监听?对此,网上也有类似方案在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
- (void)addDelegate:(id)delegate;
- (void)removeDelegate:(id)delegate;
- (void)removeAllDelegates;
@end
让它实现
协议是为了在AppDelegate.m
里面方便直接hook调用的,(不然只能在AppDelegate.m
使用respondsToSelector:@selector()
,但是这样无法传递多个参数),不过它并不需要实现
协议,而是靠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
将方法调用转发给所有子模块,子模块只需要实现自己需要的
协议方法,进行业务处理即可,代码纯粹且单一,有利于维护。比如单独处理推送的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
消息转发套路可以改成下图这种方案:
不过此时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主要几个控制器的生命周期,如下图:
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
类里面。
将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
,在系统库
里的类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项目的设计模式有很多(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。
我是小侯爷。
在帝都艰苦奋斗,白天是上班族,晚上是知识服务工作者。
如果读完觉得有收获的话,记得关注和点赞哦。
非要打赏的话,我也是不会拒绝的。