iOS项目架构

作者: 苦笑男神 | 来源:发表于2019-12-09 15:06 被阅读0次

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

    相关文章

      网友评论

        本文标题:iOS项目架构

        本文链接:https://www.haomeiwen.com/subject/lovegctx.html