谈谈关于 iOS 的架构以及应用

一直以来想写一篇文章,可是没找到合适的主题,前段时间一直在看 Flutter 的一些东西,原本有意向想写关于 Flutter 的一些总结,可是看的有些零零散散,而且没有实际应用过,因此也就搁置了。正好最近一段时间除主业务之余,一直在作咱们 甘草医生 用户端的重构,恰好有一些对于 iOS 架构方面的见解与感悟,在这里与你们分享。 万事开头难!其实在开始重构以前,我是很纠结的,一直很难开始。我也曾翻阅过不少资料,想找到一个合适的符合咱们本身目前业务的架构,最后作了种种的比对与测试,选择了 MVVM + 组件化 + AOP 的模式来重构。可能有人会疑问,你为何选择这样的架构模式?使用这些模式有什么好处?这些抽象的模式概念具体应该怎么在实际项目中运用?OK,那咱们就带着这些疑问一步步往下看。html

关于架构模式

咱们先来了解一下在 iOS 中经常使用的一些架构模式ios

  • MVC 关于 MVC(Model-View-Controller)这个设计模式我相信稍有些编程经验的人都了解至少据说过,做为应用最为普遍的架构模式,你们应该都是耳熟能详了,可是不一样的人对 MVC 的理解是不一样的。在 iOS 中,Cocoa Touch 框架使用的就是 MVC ,以下 git

    这是苹果典型的 MVC 模式,用户经过 View 将交互(点击、滑动等)通知给 Controller,Controller 收到通知后更新 Model,Model 状态改变之后再通知 Controller 来改变他们负责的 View。因为在 iOS 中咱们经常使用的 UIViewController 自己就自带一个 View,因此在 iOS 开发中 Controller 层和 View 层老是紧密的耦合在一块儿,若是一个页面业务逻辑量大的话,一个视图控制器常常会不少行的代码,致使视图控制器很是的臃肿。 可见,MVC 模式虽然能带来简单的业务分层,可是想必各位使用 MVC 模式的 iOSer 们常常会被如下几个问题困扰

    1. 厚重的 ViewController 在平常的处理中,咱们通常将咱们的一些网络请求、数据存储、视图逻辑等一些处理所有扔在咱们的 ViewController 里,在业务量大的状况下,一个 ViewController 里面就会有几千行代码
    2. 较差的可测试性 对一个有几千甚至上万行的 ViewController 进行单元测试是一个很是难以接受的事情,能够说,谁接到这个任务都是难以接受的
    3. 较差的可读性 我相信你们都有接手一个项目而后改 bug 的经历,当你看到一个有 10000 行的代码的 ViewController 的时候,你确定吐槽过
  • MVVMgithub

    MVVM (Model-View-ViewModel),其实也是基于 MVC 的。上面咱们说的 MVC 臃肿的问题,在 MVVM 的架构模式中获得了解决,咱们一些经常使用的网络请求、数据存储等都交给它处理,这样就能够分离出 ViewController 里面的一些代码使其“减肥”。 编程

    如图,就是 MVC 到 MVVM 的演变过程,在 MVVM 中 V 包含 View 和 ViewController,能够看出来 MVVM 其实就是把 MVC 中的 C 分离出来一个 ViewModel 用来作一些数据加工的事情。在上面 MVC 模式中讲了,一个 ViewController 常常会有不少东西要处理,数据加工、网络请求等,如今均可以交给 ViewModel 去作了。这样,Controller 就能够实现“减肥”,而更加专一于本身的数据调配的工做,绑定 ViewModel 和 View 的关系
    能够看出 MVVM 的模式解决了 MVC 模式中的一些问题,使得 ViewController 代码量减小、使得可读性变高、代码单元测试变得简单。可是 MVVM 也有其一些缺陷,好比因为 ViewModel 和 View 的绑定,那么出现了 bug 第一时间定位不到 bug 的位置,有多是 View 层的 bug 传到了 Model 层。还有一点就是对于较大的工程的项目,数据的绑定和转换须要较大的成本。关于其缺点以及可行的解决方式,在 Casa TaloyumiOS应用架构谈 网络层设计方案 已经说明的比较详细,有兴趣的童鞋能够去看一下,几篇关于架构方面的文章都很值得一读。

  • 其余的一些架构模式swift

    还有一些其余的架构模式,好比 MVP(Model-View-Presenter)、VIPER(View-Interactor-Presenter-Entity-Routing)、MVCS(Model-View-Controller-Store)等,其实都是基于 MVC 思想派生出来的一些架构模式,基本都是为了给 Controller 减负而生的,因此仍是那句话,万变不离 MVC !设计模式

架构模式的选用

了解到每一个架构模式的优缺点以后,这里,我决定用 MVVM 的架构模式来重构咱们的 APP。那么说到 MVVM ,咱们就确定是要提到 RAC ,也就是 ReactiveCocoa,它是一个响应式编程的框架,可使每层交互起来更加方便清晰。固然, RAC 确定不是实现数据绑定的惟一方案,在 iOS 中好比 KVO、Notification、Delegate、Block等均可以实现,只不过是 RAC 的实现更加优雅一些,因此咱们常常会采用 RAC 来实现数据的绑定。关于 RAC ,下面一张图很清晰的解释了它的思想,也就是 FRP(Function Reactive Programming)函数响应式编程 浏览器

上图能够看到 c 根据 a 和 b 的值变化的过程。举个例子,咱们通常在登陆的时候,会限制输入手机号的长度,那么按着以往的作法,就是实现 UITextField 的代理,监听输入文字的变化,以下

//一、导入代理
<UITextFieldDelegate>
//二、设置代理
self.phoneTextField.delegate = self;
//三、实现代理
- (void)textFieldDidChange:(UITextField *)textField {

        if (textField == self.phoneTextField) {
            if (textField.text.length > 11) {
                textField.text = [textField.text substringToIndex:11];
            }
        }
}
复制代码

那么若是使用 RAC ,以下bash

@weakify(self);
    [[self.phoneTextField.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
        return value.length > 11 ? [value substringToIndex:11] : value;
    }] subscribeNext:^(NSString *x) {
        @strongify(self);
        self.phoneTextField.text = x;
    }];
复制代码

能够看出代码变得更加清晰了,咱们只须要实现对 phoneTextField 信号的监听,就能够实现了。咱们再来看一个例子,好比在咱们用户端的登陆界面,以下图 微信

按着正常的逻辑就是用户输入 11 位手机号码后再输入密码才能使其登陆,这个时候咱们的登陆按钮才能点击,要想实现这个逻辑,正常的作法应该以下,

//一、导入代理
<UITextFieldDelegate>
//二、设置代理
self.phoneTextField.delegate = self;
self.passwordTextField.delegate = self;
//三、实现代理
- (void)textFieldDidChange:(UITextField *)textField {

        if (self.phoneTextField.text.length == 11 && [self.passwordTextField isNotBlank]) {
             self.loginButton.enabled = YES;
        } else {
             self.loginButton.enabled = NO;
        }
}
复制代码

而使用 RAC 则以下

@weakify(self);
    [[[RACSignal combineLatest:@[self.phoneTextField.rac_textSignal,
                                 self.passwordTextField.rac_textSignal]] map:^id _Nullable(RACTuple * _Nullable value) {
        
        RACTupleUnpack(NSString *phone, NSString *password) = value;
        return @([password isNotBlank] && phone.length == 11);
        
    }] subscribeNext:^(NSNumber *x) {
        @strongify(self);
        if (x.boolValue) {
            self.loginButton.enabled = YES;
        } else{
            self.loginButton.enabled = NO;
        }
    }];
复制代码

咱们将 self.phoneTextField.rac_textSignalself.passwordTextField.rac_textSignal 这两个信号合并成一个信号而且监听,实现必定的逻辑,简单明了。固然, RAC 的好处远远不止这些,这里只是冰山一角,有兴趣的能够去本身用一用这个库,体验更多的功能,这里也就很少赘述了。

关于组件化

组件化这个概念相信你们都据说过,使用组件化的好处就是使咱们项目更好的解耦,下降各个分层之间的耦合度,使项目始终保持着 高聚合,低耦合 的特色。举个简单的例子,在 iOS 中页面之间的跳转,两个开发人员负责开发两个页面,小 A 负责开发的 AViewController 已经开发完毕,而后须要点击按钮跳到小 B 负责的 BViewController,而且须要传一个值,以下

//一、导入BViewController
#import "BViewController"
//二、跳转
BViewController *bViewController = [[BViewController alloc]init];
bViewController.uid = @"123";
[self.navigationController pushViewController:bViewController animated:YES];
复制代码

这时候小 A 已经准备去写其余业务了,可是一问才发现小 B 并无开始写 BViewController,还须要一段时间才能写,那么小 A 就郁闷了,要么就等着小 B 写完我再去作其余的,要么就先注释我这段代码,等到小 B 写完我再解注释。形成这种状况的缘由就是由于两个页面之间牢牢地耦合在一块儿了,在开发人员少或者独立开发的状况下咱们常用这种方式进行页面间的跳转和传值,页面基本都是一我的负责,因此感受不到问题,试想一下在几十人开发的工做组中,划分很细的状况下,你本身的脱节是否是给别人带去了没必要要的麻烦。我相信这是全部人都不想发生的,那么咱们就须要对页面进行组件化解耦,这里我所使用的组件化方案是 target-action 方式,使用的是 Casa TaloyumCTMediator,其主要的思想就是经过一个中间者来提供服务,经过 runtime来调用组件服务,好比之前的依赖关系以下

那么使用 CTMediator 实现组件化之后,各组件之间的依赖关系变成下图
这样各模块之间就实现了解耦,模块之间的通讯就所有经过中间层来进行。咱们回过头来再看以前的小 A 和小 B,若是使用这种方式,那么小 A 的跳转代码应该以下

//一、导入Mediator
#import "CTMediator+BViewControllerActions.h"
//二、跳转
UIViewController *viewController = [[CTMediator sharedInstance] gc_bViewController:@{@"uid": @"123"}];
[self.navigationController pushViewController:viewController animated:YES complete:nil];
复制代码

这样小 A 就不用管小 B 是否是写完没,也不须要导入小 B 的页面,就能够跳转到小 B 的页面,实现了页面间的解耦。能达到这一目的的功臣就是咱们的中间者,咱们来看看它作了什么,咱们仍是以咱们登陆页面为例,咱们从登录跳转到注册页面的代码以下

//一、导入Mediator
#import "CTMediator+RegistViewControllerActions.h"
//二、跳转
UIViewController *viewController = [[CTMediator sharedInstance] gc_registViewController];
[self.navigationController pushViewController:viewController animated:YES complete:nil];
复制代码

其中 CTMediator+RegistViewControllerActions.h 中的代码以下

//
// CTMediator+RegistViewControllerActions.m
// GCUser
//
// Created by HenryCheng on 2019/4/15.
// Copyright © 2019 HenryCheng. All rights reserved.
//
#import "CTMediator+RegistViewControllerActions.h"
NSString *const gc_targetRegistVC = @"RegistViewController";
NSString *const gc_actionRegistVC = @"registViewController";
@implementation CTMediator (RegistViewControllerActions)
- (UIViewController *)gc_registViewController {
        UIViewController *viewController = [self performTarget:gc_targetRegistVC
                                                        action:gc_actionRegistVC
                                                        params:@{@"title": @"注册"}
                                             shouldCacheTarget:NO
                                            ];
        if ([viewController isKindOfClass:[UIViewController class]]) {
            return viewController;
        } else {
            return [[UIViewController alloc] init];
        }
}
@end
复制代码

其中重要的就是 performTarget:action:params:shouldCacheTarget: 这个方法,内部的实现方式以下

- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget {
        NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
        // generate target
        NSString *targetClassString = nil;
        if (swiftModuleName.length > 0) {
            targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
        } else {
            targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
        }
        NSObject *target = self.cachedTarget[targetClassString];
        if (target == nil) {
            Class targetClass = NSClassFromString(targetClassString);
            target = [[targetClass alloc] init];
        }
        // generate action
        NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
        SEL action = NSSelectorFromString(actionString);
        
        if (target == nil) {
            // 这里是处理无响应请求的地方之一,这个demo作得比较简单,若是没有能够响应的target,就直接return了。实际开发过程当中是能够事先给一个固定的target专门用于在这个时候顶上,而后处理这种请求的
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            return nil;
        }
        
        if (shouldCacheTarget) {
            self.cachedTarget[targetClassString] = target;
        }
    
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 这里是处理无响应请求的地方,若是无响应,则尝试调用对应target的notFound方法统一处理
            SEL action = NSSelectorFromString(@"notFound:");
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            } else {
                // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程当中,能够用前面提到的固定的target顶上的。
                [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
                [self.cachedTarget removeObjectForKey:targetClassString];
                return nil;
            }
        }
}
复制代码

能够看到若是有响应则调用 safePerformAction:target: params: 这个方法,以下

- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params {
        NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
        if(methodSig == nil) {
            return nil;
        }
        const char* retType = [methodSig methodReturnType];
    
        if (strcmp(retType, @encode(void)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:&params atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            return nil;
        }
    
        if (strcmp(retType, @encode(NSInteger)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:&params atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            NSInteger result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
    
        if (strcmp(retType, @encode(BOOL)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:&params atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            BOOL result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
    
        if (strcmp(retType, @encode(CGFloat)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:&params atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            CGFloat result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
    
        if (strcmp(retType, @encode(NSUInteger)) == 0) {
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
            [invocation setArgument:&params atIndex:2];
            [invocation setSelector:action];
            [invocation setTarget:target];
            [invocation invoke];
            NSUInteger result = 0;
            [invocation getReturnValue:&result];
            return @(result);
        }
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [target performSelector:action withObject:params];
    #pragma clang diagnostic pop
}
复制代码

经过这两个方法咱们就能够看到整个 CTMediator 实现的思路了,为何 AViewController 不引用 BViewController 还能向其进行跳转传值,原来都是因为 runtime 在中间起做用。 固然,虽然中间者这个方案能很好地实现各页面之间的解耦,可是也有它的缺点。咱们能够看到咱们在 CTMediator+RegistViewControllerActions.h 中定义的 gc_targetRegistVCgc_actionRegistVC 这两个常量,分别对应 ‘target’ 和 ‘action’,这里面须要注意的是必定要细心,若是这儿写错,会引起未知的错误,可是编译器并不会提示,对应的 Target_...必定要和这里的 target 一致,不然就会引起错误。这种方案的实施对开发人员的细心程度是有很大要求的,由于若是有错误,在编译中没法发现的。 组件化的方案的实施还有不少其余的方案,好比 url-blockprotocol-class方式,有兴趣的能够看看蘑菇街的 MGJRouter,还有就是阿里的 BeeHive ,它是基于 Spring 的 Service 理念,使用 Protocol 的方式进行模块间的解耦。

关于 AOP

先看一个案例,小 C 最近愁眉苦脸,你发现了他状态不对劲,因而就发生了下面的对话

你:“小 C,你这是怎么啦,是否是工做上有什么不顺心的?”

小 C:“是啊,最近接到一个需求,让我很头疼!”

你:“接到需求不是很正常,作就是了啊!”

小 C:“你不知道,这个需求是统计每一个页面的浏览状况,就是用户到了这个页面我就要统计一下,
运营产品他们要看 PV,因而我就在基类里面的 `viewDidLoad` 方法加了一下,这样很简单就解决了”

小 C:“但是他们又说还要我作每一个页面按钮的点击统计,你说这 APP 几百个页面,这么多按钮,我怎么加啊,
就算我加了,个人代码也会由于这些与业务无关的代码而变得混乱,万一哪天不统计再让我删了,那我不是要命了啊!愁死我了!”

你:“那你这使用 AOP 就能够了啊”

小 C :“A...OP???”
复制代码

AOP(Aspect-oriented programming),面向切面编程,是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提升程序代码的模块化程度。在 iOS 中有一个应用很是多的轻量级的 AOP 库 Aspects ,它容许你能在任何一个类和实例的方法中插入新的代码。看到这里,你可能就已经知道小 C 的问题该如何解决了,下面是使用 Aspects 实现页面统计的代码

//
// GCViewControllerIntercepter.m
// GCUser
//
// Created by HenryCheng on 2019/4/25.
// Copyright © 2019 HenryCheng. All rights reserved.
//

#import "GCViewControllerIntercepter.h"
#import <Aspects/Aspects.h>

@implementation GCViewControllerIntercepter

+ (void)load {
    [GCViewControllerIntercepter sharedInstance];
    
}

+ (GCViewControllerIntercepter *)sharedInstance {
    static GCViewControllerIntercepter *_sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedClient = [[GCViewControllerIntercepter alloc] init];
    });
    return _sharedClient;
}

- (instancetype)init {
    if (self == [super init]) {
    
        [UIViewController aspect_hookSelector:@selector(viewDidLoad)
                           withOptions:AspectPositionAfter
                            usingBlock:^(id<AspectInfo> aspectInfo, UITouch *touch, UIEvent *event) {
                                
                                if ([aspectInfo.instance isKindOfClass:[UIViewController class]]) {
                                    
// 页面统计的代码
                                }
                            } error:NULL];
        
        [UIControl aspect_hookSelector:@selector(beginTrackingWithTouch:withEvent:)
                           withOptions:AspectPositionAfter
                            usingBlock:^(id<AspectInfo> aspectInfo, UITouch *touch, UIEvent *event) {
                                
                                if ([aspectInfo.instance isKindOfClass:[UIButton class]]) {
// 按钮统计的代码
                                    
                                }
                            } error:NULL];
        
        
    }
    return self;
}

@end
复制代码

咱们能够看到,经过新建 GCViewControllerIntercepter 这个类就实现了页面的统计和按钮点击统计功能,你只须要实现就行,连导入都不用,若是哪天你不须要这些统计的代码了,你直接从项目中移除这个类就能够了。是否是很简单!这就是 AOP 的一个使用实例,经过 + (void)load 这个方法(+ load 做为 Objective-C 中的一个方法,与其它方法有很大的不一样。它只是一个在整个文件被加载到运行时,在 main 函数调用以前被 ObjC 运行时调用的钩子方法),实现了 GCViewControllerIntercepter 这个类被调用,而后经过 Aspects 实现对 UIViewController 和 UIControl 的 hook。这样在每一个页面被加载、每一个按钮被点击以前这边就能够捕捉到。 还有就是有人提到过去基类,也就是抛弃厚重的 base ,直接使用 AOP ,这样的话好比我想写个新 demo 就不用引入各类父类了,直接 hook 拿来用就行了。这种方法我的以为没有到大工程的时候仍是用继承来实现比较好。若是工程量比较大便于各个开发人员调试,可使用这种方法。 固然 AOP 的做用也不只如此,这里就说这么一个咱们经常使用的 hook 的例子,有兴趣能够下去好好了解下。

一、AspectOptions 有四个值,分别是 AspectPositionAfterAspectPositionInsteadAspectPositionBeforeAspectOptionAutomaticRemoval,这样你能够决定你 hook 的位置

二、对于 + (void)load 还有 + (void)initialize 这两个方法不是太了解的童鞋能够看看大左 Draveness你真的了解 load 方法么?懒惰的 initialize 方法 这两篇文章,了解这两个方法相信对你会颇有帮助

实际项目中的应用

了解了上面的内容,接下来咱们看看在实际项目中的应用

  • 项目的目录结构

    重构的项目结构如上图,相信你们一看名称就大概知道每一个文件夹是作什么的,因为 ModelViewViewControllerViewModel 这几个类联系比较紧密,因此建议这几个类的项目结构保持一致,以下图
    这样目录一目了然,好比你想找一个登陆相关的东西,那么你就知道能够在各大目录下的 Login 模块里面去寻找。并且建议目录不要过深,通常三层就够了,过深的话查找起来比较麻烦。

  • Category 的使用

    可能你们已经看到了,个人项目目录里面有一项是 AppDelegate+Config 这一项,这其实就是 AppDelegate 的一个 Category 。在 iOS 开发中 Category 随处可见,如何应用那就是看本身的需求状况了,这里我用 AppDelegate+Config 这个类来处理 AppDelegate 里面的一些配置,减小 AppDelegate 的代码,让项目更加清晰,使用了之后咱们能够看到 AppDelegate 目录的代码片断

    #import "AppDelegate.h"
     #import "AppDelegate+Config.h"
     #import "GCPushManager.h"
     
     @interface AppDelegate ()
     
     @end
     
     @implementation AppDelegate
     - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
     
         [self configTabbar];
         [self registWeChat];
         [self configNetWork];
         [self configJPushWithLaunchOptions:launchOptions];
         [self configKeyboard];
         [self configBaiduMobStat];
         [self configShareSDKWithLaunchOptions:launchOptions];
         return YES;
     }
     // between iOS 4.2 - iOS 9.0
     - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation {
         [self handleOpenURL:url];
          return NO;
     }
     // after iOS 9.0
     - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
          [self handleOpenURL:url];
          return NO;
     }
    复制代码

    这样代码看起来很清晰,相信你们都有过一打开 AppDelegate 这个类看到一大堆代码,东找西找很不规范的经历。因为是项目重构初期,AppDelegateAppDelegate+Config 使用的比较多,暂时先放在这里,后期再将其移动到合适的位置。

  • CocoaPods 的使用

    相信这个东西你们都用过,为何要强调一下 CocoaPods 的使用,由于在我整理以前项目时发现,有的地方(好比微信支付、支付宝支付)就是直接将 lib 直接拖进工程,有的还须要各类配置,这样若是升级或者移除的时候就很麻烦。使用 CocoaPods 管理的话那么升级或者移除就很方便,因此建议仍是能使用 CocoaPods 安装的就直接使用其安装,最好不要直接在项目中添加第三方。 还有一种状况就是有时候第三方知足不了咱们的需求,须要修改一下,因此有些就不集成在 CocoaPods 里面了(万一一不当心 update 之后修改的内容被覆盖)。这里我想说的是,对于这种状况你仍然可使用 CocoaPods,那么怎么解决须要修改代码的问题?没错,就是 Category !

  • MVVM的运用

    具体项目的实现咱们仍是以登陆为例,在 ViewModel 中

    - (void)initialize {
          [super initialize];
          RAC(self, isLoginEnable) = [[RACSignal combineLatest:@[
                                                                 RACObserve(self, phone),
                                                                 RACObserve(self, password)
                                                                 ]] map:^id _Nullable(RACTuple * _Nullable value) {
                                          RACTupleUnpack(NSString *phone, NSString *password) = value;
                                          return @([phone isNotBlank] && [password isNotBlank] && phone.length == 11); }];
          
          RAC(self.loginRequest, params) = [[RACSignal combineLatest:@[
                                                          RACObserve(self, phone),
                                                          RACObserve(self, password)
                                                          ]] map:^id _Nullable(RACTuple * _Nullable value) {
                                 
                                   RACTupleUnpack(NSString *phone, NSString *password) = value;
                                       return @{@"phone": GC_NO_BLANK(phone),
                                                @"pwd": GC_NO_BLANK(password)
                                                }; }];
     }
     - (RACCommand *)loginCommand {
          if (!_loginCommand) {
              @weakify(self);
              _loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
                  @strongify(self);
                  return [self.loginRequest requestSignal] ;
              }];
          }
          return _loginCommand;
     }
    复制代码

    这里咱们作了网络的请求以及一些数据的绑定,在 ViewController 中

    - (void)gc_bindViewModel {
         [super gc_bindViewModel];
         
         RAC(self.viewModel, phone) = self.loginView.phoneTextField.rac_textSignal;
         RAC(self.viewModel, password) = self.loginView.passwordTextField.rac_textSignal;
         RAC(self.loginView.loginButton, enabled) = RACObserve(self.viewModel, isLoginEnable);
         
         @weakify(self);
         
         [[[self.loginView.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside] throttle:0.25] subscribeNext:^(__kindof UIControl * _Nullable x) {
             @strongify(self);
             [self.viewModel.loginCommand execute:nil];
         }];
         
         [[self.viewModel.loginCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) {
             if (x.boolValue) {
                 [GCHUDManager show];
             } else {
                 [GCHUDManager dismiss];
             }
         }];
         // 登陆命令监听
         [self.viewModel.loginCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *x) {
             @strongify(self);
             UserModel *userModel = [UserModel modelWithDictionary:x];
             [[GCCacheManager sharedManager] updateDataWithDictionary:x key:GCUserInfoStoreKey()];
             [GCPushManager gc_setAlias:x[@"phone"]];
             if (userModel.is_agree.intValue == 0) {
     // 未赞成甘草协议
     
             } else if (userModel.is_agree.intValue == 1 && userModel.pwd_status.intValue == 0) {
     // 赞成协议可是没改过密码
     
             } else {
     // 登陆
             }
         } error:^(NSError * _Nullable error) {
             
         }];
    }
    复制代码

    能够看到 ViewController 将 View 和 ViewModel 进行了绑定,而且当登陆按钮点击的时候监测登陆信号的变化,根据其信号执行的开始和结束来控制 HUD 的显示和消失,而后再根据信号的返回结果来处理相关的登陆配置和跳转(极光推送的登陆、根据状态执行跳转逻辑等)。这里网络的请求都是在 ViewModel 中进行的,ViewController 只负责处理ViewModel、View 和 Model 之间的关系。

  • DRY

    DRY(Don't repeat yourself),能封装起来的类必定要封装起来,到时候使用也简单,千万不要为了一时之快而各类 ctrl + cctrl + v,这样会使你的代码混乱不堪,这其实也是项目臃肿的一个缘由。在重构的过程当中就封装了不少的类,管理起来很方便

一些感想

其实最开始的时候一直都有重构的想法,可是迟迟没有动手。其中一个缘由就是不知道该如何动手,不知道该使用什么工具,该使用哪一种方案。等到真正开始的时候发现其实没有想象中的那么难,因此当你有想法的时候你就去作,在作的过程当中你能够慢慢体会。 在重构以前,我又从新读了一下代码规范,也就是 禅与 Objective-C 编程艺术 这本书,并在重构的过程当中严格执行,好比 loginButton 就毫不会写成 loginBtn,相信我,按着规范来,你会体会到其中的意义的。 在作一个 APP 以前,在咱们新建工程的时候,就应该已经肯定你的架构模式,而且在之后的业务处理中,严格的按着这种设计模式执行下去。若是在前面需求量很少的时候你还能按着最初的设计模式执行下去,在业务忽然增多的时候,为了偷懒省事,直接各类代码混乱的糅合在一块儿,各类 ctrl + cctrl + v,致使架构的混乱引发蝴蝶效应,那么这个架构在后期若是再想从新规范起来将会是个费时费力的过程。因此,在最初设计的时候咱们就应该肯定架构方案,以及严格的执行下去。 还有就是平时的一些技术积累以及知识存储。知其然知其因此然,研究技术背后的底层原理,会对你有很大的帮助。好比说我要说来讲说 ViewController 的生命周期,可能你们都会随口说出 viewDidLoadviewWillAppear 等,我要问说说 View 的生命周期,可能就会有少数人茫然了。这些都是很基本的东西,可能你平时用不到,可是仍是须要你去了解他,注意细节。不少人可能会常常有这样的困惑,好比我想写一个图片浏览器,可是我不知道该如何写?写完了性能如何?别人是怎么写的?这个就是须要平时的积累了,好比关于 UIText 相关的的你就得想到 YYText,数据存储方面的你不只要知道老的 fmdb ,微信开源的 wcdb 有没有去了解下呢?好比我就平时没事喜欢在 GitHub 上看一些 star 比较高的开源库,看看别人是怎么实现的,想一想在个人项目中怎么使用。举个例子,最近阿里开源的 协程 框架 coobjc ,就在项目中使用,用来判断用户是否登陆

- (void)judgeLoginBlock:(void(^)(GCLoginStatus status))block {

    co_launch(^{
        NSDictionary *dic = await([self co_loginRequest]);
        if (co_getError()) {
            block(GCLoginStatusError);
        } else if (dic) {
            if ([dic[@"status"] intValue] == 1) {
                block(GCLoginStatusLogin);
            } else if ([dic[@"status"] intValue] == -99) {
                block(GCLoginStatusUnLogin);
            } else {
                block(GCLoginStatusError);
            }
        } else {
            block(GCLoginStatusError);
        }
    });
}
复制代码

一眼看去逻辑就很简单明了,比 Block 嵌套 Block 这种方式优雅的多。 如今只是重构的开始,如今已经完成的登陆的重构就 LoginViewController 而言,与以前相比就已经有很大的改变了(以前将近 800 行代码,重构后只有 200 行),可能整体上各个模块代码加起来都差很少,可是为 ViewController 减负后更加清晰明了了。后面重构完成后会出一个代码量、包大小、性能等的对比,到时候再与你们分享!

Reference

一、浅谈 MVC、MVP 和 MVVM 架构模式

二、iOS应用架构谈 view层的组织和调用方案

三、iOS 如何实现Aspect Oriented Programming

四、CTMediator

五、BeeHive

六、objc-zen-book

七、coobjc

相关文章
相关标签/搜索