iOS 架构设计代码实例学习-MVC 模式

2,317 阅读10分钟

项目Demo

"MVC",即Model(模型),View(视图),Controller(控制器)。

如何设计一个程序的结构,这是一门专门的学问,叫做"架构模式"(architectural pattern),属于编程的方法论。MVC 模式就是架构模式的一种

它是Apple 官方推荐的 App 开发架构,也是一般开发者最先遇到、最经典的架构。

1 为什么要学习并使用架构模式?

首先,作为一名合格的程序猿,我们在写代码的时候都应该追求。比如在敲每一行代码的时候,都应该注重代码规范,写出一份看得舒服,让别人也看得懂的代码,这样也能提高效率;比如在设计代码的时候,应该追求的是我怎样才能写出一个好的架构(App 架构类似于现代建筑的脚手架或者是地基,一旦确定,剩下的工作就是在现成的App里添砖加瓦),让我的代码模块化,分工明确,从而提高我的工作效率

  • 总结一下什么是好的架构:高内聚,低耦合。 代码均摊,易于扩展,具有易用性。

在长期的写代码、犯错、改正、重构的过程中,会真正领悟到这句话的含义。

可以说,代码规范,架构模式设计模式是检验一个程序猿水平的重要参考。

希望我们都能写出优雅的代码!

看完了头顶的天空和方向,我们来看看脚下的土地。

我们之前主要是学习UI,我们努力让这些控件更好,更和谐的呈现在屏幕上,这很重要。

但是,我们创建一个控件,设置这个控件的样子,设置这个控件的交互方法,展示这个控件,都需要一定的代码量,控件少的时候还好,看得过去,但是控件一多,像下面这个App 的界面,各种控件就多起来了,这个时候如果还把他们都堆在一个ViewController 里面就不合适了,要改bug,要添加控件就会变得非常麻烦。

image.png

而且,市面上的几乎全部的App 都是需要联网运行的,因为联网会得到数据,可以说,没有网络数据的App 是死的,只有有了网络数据,这个App 才能真正“活”起来。那由此产生的一些问题比如,我们要怎么通过代码请求网络,得到数据后又要怎么进行处理?

而MVC架构,就是用来解决这些问题的。

我会以微信App 为demo,了解MVC,并且简单的运用MVC 模式。

2 MVC简单介绍

MVC模式的基本思想:将应用程序分成三个部分,使它们各自负责不同的功能。MVC 是一种软件架构模式,它的目的是将应用程序的数据、用户界面和控制逻辑分离开来,以便各个部分可以独立地演变和修改。

  • M:Model(模型)负责处理数据,以及处理部分的业务逻辑

通俗来说,就是你的程序是什么,就是你的程序将要实现的功能,或者是它所能干的事情。也就是微信消息列表里的人名字,信息内容,头像,是否屏蔽该人的消息等等数据,可以认为,Model 里面装满了这个程序的各种数据,它负责处理数据、以及处理部分的业务逻辑。

  • V:View(视图)负责数据的展示和事件捕捉

通俗来说,在屏幕上你所看到的,这里有一个UITableView,TableView 里面有UILabel,UIImageView,你在屏幕上看到的组件,都可以归类为View。

  • C:Controller / ViewController / VC(控制器)负责协调Model 和 View,处理大部分逻辑

它将数据从Model 层传送到View 层并展示出来,同时将View 层的交互传到Model 层以改变数据。大部分的逻辑操作(点击Button就是一种逻辑)都应该交由VC完成。(有少部分的逻辑处理交由Model 完成,这是下文中我要提到的胖Model 和瘦Model)

另外,一个程序里面可能不止一个VC,一个VC 里面也可以有多个View 和Model,View 和Model 也不是一一对应的关系。

通俗来说,就是如何使你的模型呈现给用户,比如让View 上呈现Model 的数据,就是Controller 的工作。所以你可以把Controller 看成是连接Model 和View 的桥梁。

⚠️Model 和 View 是相互独立的

这是很容易犯错的一点,因为MVC 架构模式是在软件设计中通用的,不只是iOS 开发。iOS 开发中的MVC模式是基于传统的MVC 架构的,只是在具体实现上有所不同。Apple 官方对于iOS开发中的MVC 模式和传统的MVC 架构有所不同,如果查阅传统的MVC 架构会发现,View 和Model 之间是有通信的。

关于为什么Model和View要相互独立: iOS开发基础:苹果的MVC模式

来自Apple官方的MVC图解

image.png

胖Model 和瘦Model

我们刚刚说了Model有时候不但是数据源,有时候也会处理部分的业务逻辑。这种情况就是当Model 里面有很多原始数据,但View希望展示的数据是经过加工的数据,那么这个加工的过程到底应该放在VC里面还是Model里面呢,来举个栗子说明:

View想展示今天的日期,Model拿到的原始数据是20221124,但是View希望展示的数据是2022年11月24日。

所以不难想象,20221124这串数字需要经过一定的加工,才会变成我们想要的2022年11月24日

但是这个加工过程应该放在哪里呢?是VC还是Model里面?

开发者们也思考过这个问题,因此产生了胖Model (Fat Model)瘦Model (Thin Model)

  • 胖Model对应的是瘦的VC(Skinny Controller),在Model 中 对数据进行处理 ,让Controller可以直接使用经过处理后的数据。
  • 瘦Model对应的是胖的VC(Fat Controller),Model中的数据 不进行任何处理或修改 ,原封不动的把服务器返回内容发送给Controller。

还是用刚刚的栗子说明,胖Model对应的是把这个加工过程放在Model里面(所以Model胖了),相反,瘦Model就是把加工过程放在VC里面。

这两种模式都有各自的利弊,下面这篇文章中有详细介绍:你真的了解MVC吗?

这里也给出Apple官方对MVC的介绍 Model-View-Controller

斯坦福大学公开课对MVC的介绍视频:「斯坦福大学ios开发」

图解

image.png

3 简单运用MVC

3.1 建立MVC框架

如图,一个最简单的MVC架构:

image.png

3.2 View

View视图是由一个个控件组成的吗,我们先在View里面创建一个个控件吧

因为是逆向开发,所以我们先来观察我们的目标:一个TableView,里面含有多个TableViewCell,那我们来观察每个cell:

image.png

可以看到,每个cell其实里面的控件还是很多的,在这个时候,我们用官方的cell的装备就已经不能满足我们的需要了,于是我们可以想到,使用自定义cell:这就是为什么刚刚在建框架的时候我们选择继承自UITableViewCell 像UI控件的初始化,颜色,字体,字号,位置都应该在View中设置 先来写出我们需要的控件,因为我们需要在VC中对其进行内容赋值,所以暴露在.h文件中

//  FirstPageTableViewCell.h

@interface FirstPageTableViewCell : UITableViewCell

@property (nonatomic, strong) UILabel *personLab;

@property (nonatomic, strong) UILabel *tLab;

@property (nonatomic, strong) UIImageView *imgView;  // 不要命名为image/img

@property (nonatomic, strong) UILabel *dateLab;

@property (nonatomic, strong) UIImageView *bellImageView;

@property (nonatomic, strong) UIView *separator;

@end

接下来我们在.m文件中重写其初始化方法,并且把懒加载也都写上(这里只举imgView一个例子)

// FirstPageTableViewCell.m

@implementation FirstPageTableViewCell

- (instancetype)init {
    self = [super init];
    if (self) {
        self.backgroundColor = UIColor.whiteColor;
        [self.contentView addSubview:self.imgView];
        [self.contentView addSubview:self.personLab];
        [self.contentView addSubview:self.tLab];
        [self.contentView addSubview:self.dateLab];
        [self.contentView addSubview:self.separator];
        [self.contentView addSubview:self.bellImageView];
    }
    return self;
}

#pragma mark - Method

- (void)setPosition {
    [self.imgView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(58, 58));
        make.centerY.equalTo(self.contentView);
        make.left.equalTo(self.contentView).offset(20);
    }];
}

#pragma mark - Getter 

// 懒加载
- (UIImageView *)imgView {
    if (_imgView == nil) {
        _imgView = [[UIImageView alloc] init];
        _imgView.frame = CGRectMake(20, 15, 58, 58);
        _imgView.layer.masksToBounds = YES;
        _imgView.layer.cornerRadius = 5;
    }
    return _imgView;
}
@end

3.3 Model

首先是.h文件,Model应该是反应数据的地方,因此Model里面应该设定和上面的数据类型相同的属性

下面的FirstPageModelWithDic方法则是反映了Controller与Model之间的通信,这个方法会返回一个含有数据的model,Controller将会接收这个model,以便后面供View展示。

// FirstPageModel.h
@interface FirstPageModel : NSObject

@property (nonatomic, copy) NSString *person;

@property (nonatomic, copy) NSString *text;

@property (nonatomic, copy) NSString *image;

@property (nonatomic, copy) NSString *date;

/// 是否屏蔽此人消息
@property (nonatomic) NSNumber *bell;

/// 字典转模型
/// @param dic 字典
- (instancetype)FirstPageModelWithDic:(NSDictionary *)dic;

@end

FirstPageModelWithDic方法中,我们使用了KVC 的方法字典转模型

/// KVC字典转模型

/// @param dic 字典

- (instancetype**)FirstPageModelWithDic:(NSDictionary *)dic {
    [self setValuesForKeysWithDictionary:dic];
    return self;

}

最好再加上防止崩溃

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {

 }
- (id)valueForUndefinedKey:(NSString *)key {
    return nil;
}

3.4 Controller

Controller 是整个MVC框架中最重要的一环,它控制着View 和Model,与它们保持直接通信

因此,我们第一步就是把View 和Model import进来

可以回想到,我们之前创建View 和Model 的过程中,都是没有在View中 import Model,在Model 中 import View 的,因为它们是独立,不直接通信的。

3.4.1 从Model里面拿到数据

这次的数据是从plist 文件中取得,首先因为我们的目标是消息列表,里面有很多条相像的数据,所以数据的存储方式应该为里面存放了Model 的数组NSArray

/// 数据
@property (nonatomic, strong) NSArray<FirstPageModel *> *dataArray;
- (NSArray<FirstPageModel *> *)dataArray {
    if (_dataArray == nil) {
        // 从plist文件中加载
        NSString *path = [[NSBundle mainBundle] pathForResource:@"firstPageData.plist" ofType:nil];
        NSArray *dataArray = [NSArray arrayWithContentsOfFile:path];
        NSMutableArray *ma = [NSMutableArray array];
        for (NSDictionary *dic in dataArray) {
            FirstPageModel *model = [[FirstPageModel alloc] init];
            [model FirstPageModelWithDic:dic];
            [ma addObject:model];
        }
        _dataArray = ma;
    }
    return _dataArray;
}

3.4.2 控制着View

因为我们主要的View 是TableView,我们可以通过UITableViewDataSourceUITableViewDelegate来与View 保持通信

#pragma mark - UITableViewDataSource 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

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

Controller 把从Model 那里拿到的数据给View 展示,这就完成了数据从Model 到Controller 再到View的过程

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
    FirstPageModel *dataModel = self.dataArray[indexPath.row];
    // 复用
    static NSString *firstPageCellID = @"firstPageCellID";
    FirstPageTableViewCell *firstCell = [tableView dequeueReusableCellWithIdentifier:firstPageCellID];
    if (firstCell == nil) {
        firstCell = [[FirstPageTableViewCell alloc] init];
        // VC把从Model那里拿到的数据给View展示
        firstCell.personLab.text = dataModel.person;
        firstCell.tLab.text = dataModel.text;
        firstCell.imgView.image = [UIImage imageNamed:dataModel.image];
        firstCell.dateLab.text = dataModel.date;
        // 是否屏蔽信息
        // 把NSNumber转换为NSInteger
        // 把对数据的处理放在了Controller里面,这种是瘦Model,胖VC的做法
        NSInteger isBell = [dataModel.bell integerValue];
        if (!isBell) {  // 如果不屏蔽,就把屏蔽的图案移除
            [firstCell.bellImageView removeFromSuperview];
        }
        //设置cell无法点击
        [firstCell setSelectionStyle:UITableViewCellSelectionStyleNone];
    }
    return firstCell;
}

当然,还有代码没有展示完,但是最能体现MV 思想的已经呈现完成。完整的代码请看Demo

4 其他重要的架构

前面说了,MVC 架构是最基本的架构,是一般开发者最先遇到的架构,但是随着业务和逻辑的不断增多,MVC 的一些例如Controller 代码臃肿等弊端也逐渐显现,这时候MVP,MVVM,VIPER等架构也应运而生,这些架构在公司里面的实际开发中经常被使用。

5 项目Demo

WeChat-MVC-github