78

iOS架构入门 - MVC模式实例演示

 5 years ago
source link: http://www.cocoachina.com/ios/20180727/24340.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

22qmM3u.png!web

image.png

MVC模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观

  • 控制器(Controller)--> 负责转发请求,对请求进行处理。

  • 视图(View) --> 界面设计人员进行图形界面设计。

  • 模型(Model) --> 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。

以上出自维基百科资料,下面说点人为描述(简单易懂的)~

  • Model层: 数据处理层,包括网络请求,数据加工

  • View层: 所有App上看得到的界面

  • Controller层: Model 与 View层的中介,把Model数据在View上展示出来

  • 目的: 低耦合,可复用

EJfqquq.jpg!web

image

先看这张图,这张图是iOS的MVC架构中最经常出现的图了吧,因为IOS中的Controlller 是  UIViewController,所以导致很多人会把视图写在Controller中,如下图:

@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //setupUI
    
    //1.createView
    UIView *view = [[UIView alloc]init];
    view.frame = CGRectMake(100, 100, 100, 100);
    view.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:view];
    
    //2.createButton
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeInfoDark];
    btn.center = self.view.center;
    [self.view addSubview:btn];
    
    //3...
}

这种写法在我刚蹭到iOS的时候也这样写过,先说这样写的好处,以及初学者为什么会这么写:

  1. 比如按钮,可以在当前控制器直接add target:添加点击事件,在当前控制器内就能调用到点击方法,不需要设置代理之类的;

  2. 比如要找某个界面,直接切到这个界面对应的controller就行,因为View 写在 Controller里面,不用去别的地方找,就这里有;

  3. 比如一个View,里面有一张图片,图片依赖于网络资源,这样写的好处,可以直接让 View 在 Controller 中就能拿到资源,不需要传值

缺点!!:

  • 导致Controller特别臃肿,里面代码特别多,视图一复杂起来,代码量可能过1000行,不好维护

  • 写在Controller里无法复用,除非你在 VC2里面 copy 当前VC中的 View的代码

  • 特别low!!会被懂架构的人瞧不起,喷你根本不是MVC,是MC架构,可能还要你来段喊麦证明一下自己(-。-)

如何告别MC模式,真正走到MVC?

  1. 先给自己洗脑,iOS的Controller不是UIViewController,而是普通的Controller,没有View。(很关键的一步)

  2. 模块化划分,每个模块对应自己的一个View,例如Demo2模块,View层里面有个Demo2View,将界面元素写到View中

知识1:如何传值(参数)

//View
+ (instancetype)viewWithTitleStr:(NSString *)titleStr{
    
    //do createView
    //...
}

//Controller
@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    /*setupUI*/
    //1.createView - 参数通过`View`的函数作为外部参数传进去
    DemoView *view = [DemoView viewWithTitleStr:@"我是参数"];
    [self.view addSubview:view];
}

知识2:控件点击事件如何回调给控制器

//View
@implementation DemoView

- (instancetype)initWithTitleStr:(NSString *)titleStr{
    if (self = [super init]) {
        
        UIButton *btn = [UIButton buttonWithType:UIButtonTypeInfoDark];
        [self addSubview:btn];
        [btn addTarget:self action:@selector(p_clickBtn:) forControlEvents:UIControlEventTouchUpInside];
    }
    return self;
}

- (void)p_clickBtn:(UIButton *)sender{
    
    //通过代理回调
    [_delegate respondsToSelector:@selector(clickBtn:)] ?
    [_delegate clickBtn:sender] : nil;
}

//Controller
@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //setupUI
    
    //1.createView
    DemoView *view = [DemoView viewWithTitleStr:@"我是参数"];
    view.delegate = self;
    [self.view addSubview:view];
}

#pragma mark - privateDelegate
- (void)clickBtn:(UIButton *)sender{
    //View层按钮的点击事件回调~
}

vIb2i2I.jpg!web

image

接下来看这张iOS MVC架构图二,这张也是特别常见,在上面解决了View层之后,我们来看下这里的Model层~

@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //loadDatas
    [[AFHTTPSessionManager manager]GET:url
                            parameters:parameters
                              progress:nil
                               success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
    {
        
        //刷新tableView
        _datas = responseObject;
        [_tableView reloadDatas];
        
    } failure:nil];
}

这种写法在我刚蹭到iOS的时候又这样写过,先说这样写的好处,以及初学者为什么会这么写:

  1. 简单,网络请求完,直接在当前控制器刷新TableView的数据源

  2. 比如要找某个界面的网络请求,直接切到这个界面对应的controller就行,因为数据请求 写在 Controller里面,不用去别的地方找,就这里有;

  3. 比如当前网络请求接口,需要外部参数,比如前一个界面的uuid,这样写的好处,可以直接让当前请求在 Controller 中就能拿到资源,不需要传值

缺点!!:

  • 又导致Controller特别臃肿,里面代码特别多,如果当前控制器需要多次请求,代码量可能过1000行,不好维护

  • 写在Controller里无法复用,除非你在 VC2里面 copy 当前VC中的 网络请求的代码

  • 如果某些接口有依赖要求,接口1请求完再请求接口2,嵌套起来,辣眼睛的程度差点治好我多年的近视

  • 特别low!!会被懂架构的人瞧不起,喷你根本不是MVC,如果你还用了上面的View写在Controller的操作的话,恭喜你,最终大法 - Controller架构顺利完成,并不需要什么Model && View

eyiuI3J.png!web

image

如何告别VC模式,真正走到MVC?

  1. 不用洗脑,给自己一个大耳刮子让自己清醒清醒,这iOS的Controller就算是UIViewController,也没看到M啊,没有Model。(很关键的一步)

  2. 模块化划分,每个模块对应自己的一个Model,例如Demo2模块,View层里面有个Demo2Model,将网络请求&&数据处理写到Model中

知识1:如何传值(参数)

@implementation DemoModel

+ (NSArray *)fetchDatasWithUUid:(NSString *)uuid{

    //Model发送网络请求
    NSDictionary *parameters = @{@"uuid":uuid}
        [[AFHTTPSessionManager manager]GET:url
                                parameters:parameters
                                  progress:nil
                                   success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
        {
            //这是异步请求,无法return array
        } failure:nil];
    
}

知识2:如何回调(网络请求是异步请求) - 通过Block

//Model
@implementation DemoModel

+ (void)fetchDatasWithUUid:(NSString *)uuid success:(successBlock)block{

    //Model发送网络请求
    NSDictionary *parameters = @{@"uuid":uuid}
        [[AFHTTPSessionManager manager]GET:url
                                parameters:parameters
                                  progress:nil
                                   success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject)
        {
            //通过block异步回调~
            block(responseObject);
    
        } failure:nil];   
}

//Controller
@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //loadDatas
    [DemoModel fetchDatasWithUUid:_uuid success:^(NSArray *array) {
        _datas = array;
        [_tableView reloadDatas];
    }];
}

基础的MVC讲解完毕,其实本质上就是让Controller减压,不该控制器管的他别让他知道,如上基础MVC操作之后的优势:

  • MVC架构分明,在同一个模块内,如果视图有问题,找到该模块的View就行,其他同理,Controller代码大大减少,负责View的代理事件就可以

  • 可以复用,比如你一个产品列表的数据,首页也要用,产品页也要用,直接分别在其对应的VC1 && VC2 调用函数[ProductModel fetchDatas]即可,无需写多次,View的复用同理

  • 结构分明,便于维护,拓展也是在此基础上拓展,代码干净简洁。

进阶讲解 - MVC 配合 继承,进阶提高效率

  • 常用的方法,抽一个基类出来,继承是子类可以拥有父类的方法,重新父类的方法即可,无需声明

//数据基类
@interface MNBaseDatas : NSObject

//请求数据成功
typedef void (^MNsuccessBlock)(NSArray *array);

+ (void)fetchDatasSuccessBlock:(MNsuccessBlock)block;

+ (void)fetchDatasSuccessBlock:(MNsuccessBlock)block
                  failureBlock:(MNfailureBlock)failure;

如果,如果抽出一个数据模型的基类,比如这里的MNBaseDatas,如之前我们举例的DemoModel就无需声明

@interface DemoModel : MNBaseDatas

/**继承自MNBaseDatas,父类有的就可以不用声明,这里的block 和 类方法都可以不用声明*/
//typedef void (^successBlock)(NSArray *array);

//+ (void)fetchDatasSuccessBlock:(MNsuccessBlock)block;

@end

//Controller
@implementation DemoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //loadDatas - DemoModel没有声明 -fetchDatasSuccessBlock,一样可以调用,因为父类有此方法
    [DemoModel fetchDatasSuccessBlock:^(NSArray *array) {
        _datas = array;
        [_tableView reloadDatas];
    }];
}

如果父类没有的方法或属性,在子类里面写就行了,不会影响到父类代码,父类一般也是放公共,常用的方法(或属性),如果是特殊的,直接在子类里面新增即可,无需添加到父类~

>控制器也可以使用继承,可以减少不少冗余代码

//基类控制器
@interface MNBaseViewController : UIViewController

@property (nonatomic, weak)UITableView *tableView;

@property (nonatomic, copy)NSArray *datas;

- (void)setupUI;

- (void)loadDatas;

@end

//MNBaseViewController.m 文件
@interface MNBaseViewController ()
<
UITableViewDelegate,UITableViewDataSource
>

#pragma mark - setupUI
- (void)setupUI{
     //统一创建tableView,设置当前代理=self
    UITableView *tableView = [[UITableView alloc]init];
    tableView.frame = Frame(0, DefaultNaviHeight, ScreenW, ScreenH - DefaultNaviHeight);
    tableView.delegate = self;
    tableView.dataSource = self;
}
  • 根据我们的封装,基本上所有的控制器都需要设置界面 setupUI 获取数据 loadDatas,所以将这两个函数抽到基类MNBaseViewController 中

  • 因为iOS中,tableView应该算最常见的控件之一,基本上大多数界面都会用它展示数据,所以tableView也抽到基类中,当公告属性

  • 有tableView 就跑不了数据源了,datas 同理,也抽到基类

  • 同时,设置MNBaseViewController成为 tableView 的delegate和dataSource,所有的子类都无需再声明

  • 如果有需要用到tableView的,一个[super setUI]就能拥有这个tableView,无需创建

这样,所有的UIViewController,只要继承自MNBaseViewController的,都可以有如上的函数和方法(可以根据需要扩充)

>进阶的MNBaseViewController

//继承自`MNBaseViewController`
/*数据结构是 - @[],没有section的tableVIew*/
@interface MNBaseControllerTypeNoSection : MNBaseViewController

@end

/*数据结构是 - @[@[]],有section的tableVIew*/
@interface MNBaseControllerTypeHadSection : MNBaseViewController

@end
//实现
/**没有section的tableVIew**/
@implementation MNBaseControllerTypeNoSection

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
//没有section,直接返回数据源count
    return self.datas.count;
}
@end

/**有section的tableVIew**/
@implementation MNBaseControllerTypeHadSection

-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{

    return self.datas.count;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return [self.datas[section] count];
}

@end

如上面的两个基类MNBaseControllerTypeHadSection,MNBaseControllerTypeNoSection,根据我们自己需要的数据源,选择继承自哪个类,他们拥有父类MNBaseViewController的所有属性,他们的子类,也都无需在写比如-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section、-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView之类的方法等,大幅度减少冗余代码~

2i2qIbn.png!web

demo示例0.png

muERNfA.png!web

demo示例1.png

Fnua2mU.png!web

demo示例2.png

MFZVzmJ.png!web

demo示例3.png

如图我随机抽了几个界面出来,可能有部分人都有做过类似的界面,通过合理的架构,大部分控制器代码可能也就100行,详情可见 Demo ~

总结

对于架构来说,仁者见仁智者见智,每个人都有一套适合自己的,并不是说MVC有多low,MVVM甩用MVC 技术10086条街,主要还是根据项目,根据自己的使用慢慢进阶。

下面有我一个最近花了几个小时抽出来的 Demo ,当然实际开发中的,可能Controller的代码会多一些,因为有些点击事件的代码我都是封装调用的,再放进去感觉很容易让看的人跑偏,所以点击事件基本都注掉了。但是,秉着这种思想,其实我最近写了一个多重过滤袋滑动多控制器的界面,界面相对来说比较复杂,控制器代码也才200行,总的来说还算干净。

其实TableView也可以剥离到外部,不放在Controller中,我也有Demo是那么做的,后来发现没必要,感觉还特意封出去感觉有点画蛇添足,因为我这种架构,其实tableView很多方法都在基类控制器里面的,所以Controller中的tableView代码也不会多。

MVC架构实战-Demo

欢迎star~

作者:小蠢驴打代码

链接:https://www.jianshu.com/p/309f0477aac1


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK