项目Demo
MVP 架构形式是一种常用于iOS 运用的软件架构形式。它能够帮助开发者在运用程序中完结事务逻辑和用户界面的分离,以便更轻松地办理和修正运用程序的各个部分。在这篇文章中,我将具体介绍MVP架构形式的来源、中心思想、与MVC形式的差异以及各个模块的职能和分工。
本文也将以知乎日报的App 来作为学习MVP 形式的Demo。
1 来源
之前学习过的经典MVC 形式下,有许多的优点:
- 代码明晰,职责清楚,易于保护。
- 代码复用性高,模型和视图能够在多个控制器中重复运用。
可是跟着事务的提高,代码量,复杂程度也随之提高,MVC 形式的缺陷也随之露出出来:
- 控制器往往会变得过于臃肿,逻辑代码和事务代码难以分离。
- 视图和模型之间的交互逻辑很难在多个控制器中进行同享和复用。
- 难以进行单元测验,由于控制器往往依赖于视图和模型。
总结来看,在iOS 运用的开发中,开发者一般需求处理许多事务逻辑和界面的代码,这些代码经常混杂在一起,很难保护。跟着iOS 运用程序的复杂性和规模不断添加,需求更好的架构来办理这些代码。
MVP 架构形式的诞生正是为了处理这个问题。MVP 架构形式将运用程序分成三个首要部分:模型(Model)、视图(View)和表明器或许叫模型视图和谐器(Presenter)。这种架构形式使开发者能够更好地办理代码,提高运用程序的可保护性和可扩展性。
2 简略介绍
Model
通过上图咱们也能够看出,MVP 形式下的Model 和MVC 形式下的Model 没有非常大的差异,都是担任处理运用程序的数据,包含数据的获取、存储和处理等。在MVP架构中,模型一般是指事务逻辑、数据层或网络层。 可是在数据的逻辑处理方面,相较于在MVC 形式中学习过的胖Model 和瘦Model,有了一些不同的做法。这个会在下文中提及。
View
上面的联系图中,咱们能够看到与MVC 形式非常大的不同,那便是ViewController 变成了View 层的一部分。 View 担任显示运用程序的用户界面,并向用户供给交互功用。在MVP 架构中,视图一般是指用户界面、UI 控件或视图控制器。
Presenter
这是MVP 架构形式中最重要的一点,Presenter 是Model 和View 之间的桥梁,它使得Model 和View彻底独立,在MVC 形式下ViewController 的事务代码,在MVP 形式下由Presenter 完结,使得View 层中的ViewController 能够愈加专心的完结展现UI,处理用户交互。
什么是事务代码:
1.数据处理:从Model层获取数据,对数据进行处理、转化和安排,以契合View层的需求。
这便是上文说到的在MVC 形式下原来是Model 进行的数据的处理,在MVP 形式中交给了Presenter,原因便是Presenter 要把Model 上恳求到的原始数据转化成View 层上直接能够运用的数据。
举个栗子:
假如从网络上恳求下来的原始数据是“20230421”,可是UI 上需求的是“4月21日”这种数据的处理,在MVC 形式下,能够写在Model 里边。这样能够使得ViewController 愈加专心于视图的展现和交互,而不需求关怀数据处理的具体完结。
但在MVP 形式下,Model层一般担任数据的获取、存储和处理,关于一些数据处理和存储的操作,应该放在Model 层中完结,比方数据的解析、数据的加密和解密等等。
可是关于一些事务逻辑相关的数据处理,比方对数据进行核算、筛选、排序等,这些数据处理操作或许需求考虑到多种状况,需求依据当时事务逻辑来判别处理成果,这时分就能够将这些操作放在Presenter 层中完结。由于Presenter 层是担任处理视图展现和事务逻辑的,所以它更简单了解当时的事务流程和需求,更能灵活地处理数据。
因而,把“20230412”转化成“4月12日”这种数据的处理,在MVP 形式下,应该放在Presenter 中完结,处理完后,能够直接交给View 层展现给用户。
2.事务流程:担任控制运用程序的事务流程,依据用户交互和其他条件,触发不同的事务逻辑,和谐Model和View之间的交互,确保事务流程的正确性和一致性。
3.过错处理:处理事务逻辑中呈现的过错和反常,例如网络连接失利、数据格式过错、用户输入过错等。Presenter 应该能够对这些过错进行正确处理,给用户供给友好的提示和处理方案。
举例来说,在知乎日报中,Presenter层的事务逻辑包含:
1.从Model层获取最新新闻列表数据,并依据View层的需求进行处理和安排,例如将数据依照日期分组、提取每个新闻的标题和图片等。
2.控制新闻概况页面的显示和刷新,依据用户的点击或许手势操作,触发不同的事务逻辑,例如加载更多评论、收藏新闻等。
3.处理网络恳求失利、数据解析过错等反常状况,例如当网络连接失利时,Presenter 应该能够正确处理这种反常状况,给用户友好的提示,或许主动重试连接等。
总结一些MVP 的规范
这儿供给一种现在比较多人运用的规范:
-
View
层是由UIViewController
和UIView
一起组成; -
View
层将托付Presenter
层对它自己的操作; -
Presenter
层拥有对View
层交互的逻辑; -
Presenter
层跟Model
层通讯,并将数据转化成对适应UI的数据并更新View
; -
Presenter
不需求依赖UIKit
; -
View
层是单一,由于它是被迫接受命令,没有主动能力。
3 项目代码例子
在根本了解了MVP 架构中各部门的分工后,咱们看看怎样在代码中体现MVP 形式。
有人有说,MVP 架构的中心思想是面向接口编程,调用层运用接口目标,去调用接口办法,完结层去完结接口办法,并在调用层实例化。确实,MVP 形式最精彩的部 分便是接口的运用。
Model
在Model 中,不只需恳求网络数据,一起要供给接口给Presenter。
@protocol NewsModelDelegate <NSObject>
@optional
/// 恳求Latest 成功的回调
/// - Parameter latestDayModel: 最新新闻的model
- (void)didReceiveLatestNews:(DayModel *)latestDayModel;
/// 恳求before 成功的回调
/// - Parameter beforeDayModel: 最新新闻的model
- (void)didReceiveBeforeNews:(DayModel *)beforeDayModel;
@end
@interface NewsModel : NSObject
/// 署理
@property (nonatomic, weak) id<NewsModelDelegate> delegate;
/// 获取最新新闻
- (void)fetchLatestNews;
/// 获取过往新闻
- (void)fetchBeforeNewsWithDate:(NSString *)date;
@end
@implementation NewsModel
/// 获取最新新闻
- (void)fetchLatestNews {
[DayModel getLatest:^(DayModel * _Nonnull latestModel) {
[self.delegate didReceiveLatestNews:latestModel];
}];
}
/// 获取过往新闻
- (void)fetchBeforeNewsWithDate:(NSString *)date {
[DayModel getBeforeDate:date AndModel:^(DayModel * _Nonnull beforeModel) {
[self.delegate didReceiveBeforeNews:beforeModel];
}];
}
@end
在上面的代码中,Model 为Presenter 供给了接口的完结,在Presenter中,会持有Model,(一起会恪守NewsModelDelegate
协议) 通过调用Model的fetchLatestNews
和fetchBeforeNewsWithDate:(NSString *)date
办法,会完结didReceiveLatestNews:(DayModel *)latestDayModel
和didReceiveBeforeNews:(DayModel *)beforeDayModel
办法,然后获取到网络恳求到的数据,再展现给View 层展现。
Protocol
@protocol MainProtocol <NSObject>
/// 展现最新新闻
- (void)showLatestNews:(NSDictionary *)latestModel;
/// 展现过往新闻
- (void)showBeforeNews:(NSArray *)beforeModel;
@end
这儿的协议也相当于接口,View 层恪守这个协议,而Presenter 需求一个恪守这个协议的目标,这样能愈加表达Presenter 在传递Model 层的数据时,不需求知道View 层是谁,只需求知道这是一个恪守了MainProtocol
的目标,这样也契合了咱们上面说到的MVP 规范:
-
View
层将托付Presenter
层对它自己的操作 -
Presenter
不需求依赖UIKit
使MVP 形式愈加具有可测验性,独立性。
Presenter
@interface MainPresenter () <
NewsModelDelegate
>
/// NewsModel
@property (nonatomic, strong) NewsModel *newsModel;
/// View
@property (nonatomic, weak) id<MainProtocol> view;
@end
咱们先来看.m文件的类扩展,MainPresenter
恪守了Model 的协议,代表了Presenter
成为了Model 中的一个署理,也将凭借这儿边的署理办法恳求网络上数据。一起持有一个恪守MainProtocol
的目标,而不需求关怀这个目标是谁。
随后咱们再来看看Presenter 的.h 文件
@interface MainPresenter : NSObject
/// 初始化View
- (instancetype)initWithView:(id)view;
/// 恳求最新信息
- (void)fetchLatestNewsData;
/// 恳求过往新闻列表
- (void)fetchBeforeNewsData:(NSString *)date;
@end
其间,initWithView:(id)view
办法便是绑定View,fetchLatestNewsData
和fetchBeforeNewsData:(NSString *)date
两个办法为View 供给了网络恳求的接口。
前面假如有一知半解的状况,那么往下看Presenter 的.m 文件,涉及到Presenter 与Model,Presenter 与View 的交互的具体完结,应该就会感知到一点MVP 形式的中心。
@implementation MainPresenter
#pragma mark - Life cycle
/// 初始化View
- (instancetype)initWithView:(id)view {
self = [super init];
if (self) {
// 绑定View
self.view = view;
// 绑定Model
self.newsModel = [[NewsModel alloc] init];
self.newsModel.delegate = self;
}
return self;
}
#pragma mark - Method
/// 恳求最新信息
- (void)fetchLatestNewsData {
[self.newsModel fetchLatestNews];
}
/// 恳求过往列表
- (void)fetchBeforeNewsData:(NSString *)date {
[self.newsModel fetchBeforeNewsWithDate:date];
}
从上面的代码能够看到,咱们绑定了View,Model,而且成为了Model 的署理,那么在fetchLatestNewsData
和fetchBeforeNewsData:(NSString *)date
办法中,Presenter 与Model 进行了交互,在上面介绍Model 的时分,咱们也说到了Model 里边的fetchLatestNews
和fetchBeforeNewsWithDate:
办法,在这两个办法中,咱们恳求到了网络数据,而且交给了Model 的署理,也便是传回给了Presenter。
#pragma mark - Delegate
// MARK: <NewsModelDelegate>
/// 恳求Latest 成功的回调
- (void)didReceiveLatestNews:(DayModel *)latestDayModel {
// 在这儿Model成功传回了网络信息,Presenter 应该把信息处理成View 能够直接用的姿态,传递给View
[self.view showLatestNews:(处理好的信息)];
}
/// 恳求before 成功的回调
- (void)didReceiveBeforeNews:(DayModel *)beforeDayModel {
// 在这儿Model成功传回了网络信息,Presenter 应该把信息处理成View 能够直接用的姿态,传递给View
[self.view showBeforeNews:(处理好的信息)];
}
在上面的两个署理办法中,Model成功传回了网络信息,Presenter 应该把信息处理成View 能够直接用的姿态,传递给View。咱们说过,View 和Model 应该是彻底独立的,更规范的做法,咱们不应该直接传递Model 交给View,假如要传递的内容参数比较多,咱们能够专门设置一个类来存储处理好的信息:
咱们新建了一个NewsData类,里边寄存现已处理过的信息,方便让View 直接用:
@interface NewsData : NSObject
@property (nonatomic, copy) NSString *imageURL;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *hint;
@property (nonatomic, copy) NSString *date;
@property (nonatomic, copy) NSString *idStr;
@end
建好后,咱们回到Presnter的处理Model 传回信息的办法中:
#pragma mark - Delegate
// MARK: <NewsModelDelegate>
/// 恳求Latest 成功的回调
- (void)didReceiveLatestNews:(DayModel *)latestDayModel {
// 在这儿Model成功传回了网络信息,Presenter 应该把信息处理成View 能够直接用的姿态,传递给View
// 列表数据
NSMutableArray *listMa = [NSMutableArray array];
for (DataModel *model in latestDayModel.stories) {
NewsData *data = [[NewsData alloc] init];
data.title = model.title;
data.hint = model.hint;
data.imageURL = model.imageURL;
data.date = latestDayModel.date;
data.idStr = model.ID;
[listMa addObject:data];
}
NSArray *listData = [NSArray array];
listData = listMa;
NSDictionary *dict = @{@"listData": listData};
[self.view showLatestNews:dict];
}
上面的DataModel
是Model 层的类,也便是传递给Presenter 的原始数据模型,咱们要做的便是把原始的DataModel
转化成View 需求的NewsData,之所以用dict
来储存,是由于在知乎日报的项目中,didReceiveLatestNews
办法传回来的不只有列表的数据,还有Banner 的数据,可是这儿的代码演示中仅仅简略展现了一种,在完好的Demo中,dict
不但有listData
,还应该有bannerData
。最终,Presenter 把dict 数据交给了View 展现。
上面便是Presenter 层的所有首要代码,能够看到,Presenter 承担起了MVC 形式中的事务代码,现在的VC 只需拿到数据,就能够直接展现了。因而,Presenter 的代码量也不会非常多。
View
事务代码交给了Presenter,View 就能够专心展现界面和处理用户的交互行为了。
// Presenter
#import "MainPresenter.h"
// Protocol
#import "MainProtocol.h"
#import "NewsData.h"
@interface MainVC () <
MainProtocol,
UITableViewDataSource,
UITableViewDelegate
>
/// Presenter
@property (nonatomic, strong) MainPresenter *presenter;
/// 新闻列表
@property (nonatomic, strong) UITableView *tableView;
/// 列表新闻数据
@property (nonatomic, strong) NSMutableArray *newsList;
@end
@implementation MainVC
- (void)viewDidLoad {
[super viewDidLoad];
self.newsList = [NSMutableArray array];
// 恳求最新信息
[self.presenter fetchLatestNewsData];
// list
[self.view addSubview:self.tableView];
self.tableView.dataSource = self;
self.tableView.delegate = self;
}
// MARK: <MainProtocol>
/// 展现最新新闻
- (void)showLatestNews:(NSDictionary *)latestModel {
[self.newsList removeAllObjects];
[self.newsList addObject:latestModel[@"listData"]];
[self.tableView reloadData];
}
// MARK: <UITableViewDataSource>
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.newsList.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.newsList.count == 0 ? 0 : 6;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MainTableViewCell *cell = [MainTableViewCell creatCellDefault:tableView];
if (self.newsList.count != 0) {
[cell setNormalBackground];
}
NewsData *newsData = self.newsList[indexPath.section][indexPath.row];
cell.titleLab.text = newsData.title;
cell.hintLab.text = newsData.hint;
[cell.imgView setImageWithURL:[NSURL URLWithString:newsData.imageURL] placeholderImage:[UIImage imageNamed:@"defaultImage"]];
return cell;
}
#pragma mark - Getter
- (MainPresenter *)presenter {
if (_presenter == nil) {
_presenter = [[MainPresenter alloc] initWithView:self];
}
return _presenter;
}
咱们能够看到,View 绑定了Presenter,也遵从了MainProtocol
协议,因而,当Presenter 和Model 交互,恳求到网络数据后,能够通过MainProtocol
中的showLatestNews:(NSDictionary *)latestModel
传递给View,View拿到数据后,就能够进行一系列在UI层面上的展现。
在上面的demo 中,咱们拿到数据后,储存在了newsList
里边,然后在tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
办法中展现到tableView 中。
以上是比较简略的用代码演示MVP 形式中各个部分的作业介绍。项目Demo 有项目的完好代码。
4 其他的事务处理
按钮点击事情
咱们刚刚说过,MVP 形式中,View 层要处理用户交互事情,可是假如涉及到与Model 的交互,应该交由Presenter 处理,所以,要处理按钮的点击事情,应该先判别按钮点击事情是否涉及到数据的改动。
举个栗子,假如按钮点击后仅仅造成了例如界面背景颜色的改动,或许一个动画效果,而不涉及到了数据的改动, 那么View 层不需求通知到Presenter 来处理事务,能够直接在View 层中处理该事情。
可是假如假如按钮点击后造成了例如点赞数,收藏数加一或许减一这种需求涉及到数据的改动或许需求触发其他的事务逻辑,那么最好还是通知到Presenter 层来处理,以坚持MVP 形式的分层结构。
界面跳转
依据MVP 形式的规划思路,Presenter 应该只关怀事务逻辑的完结,不直接操作View 和进行界面跳转等视图层操作。因而,当需求进行界面跳转时,Presenter 应该将跳转的恳求传递给View,由View 担任进行具体的跳转操作。
在这个过程中,Presenter 只需求关怀跳转恳求的成果是否契合事务逻辑即可。
而跳转后的界面对应的Presenter 能够在界面初始化时由View创建并绑定,与之前的Presenter 进行解绑。所以,界面间通讯应该是Presenter 与View 的通讯,而不是Presenter 与Presenter 的通讯。
还是举知乎日报项目的例子:
点击新闻列表的恣意一个cell 进入新闻概况页,这个过程中,需求传入一个新闻的id 号才能进行新闻概况的恳求,那么咱们能够想到,是音讯列表界面的View 处理了点击事情,把新闻的ID 号传给了新闻概况页模块的Presenter,再由Presenter 进行网络恳求,拿到新闻概况页的数据给View 展现。
@implementation MainVC
// 点击新闻,进入概况页
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NewsData *newsData = self.newsList[indexPath.section][indexPath.row];
NSString *idStr = newsData.idStr;
NewDetailsPresenter *presenter = [[NewDetailsPresenter alloc] initWithID:idStr];
NewDetailsVC *newDetailsVC = [[NewDetailsVC alloc] initWithPresenter:presenter];
[self.navigationController pushViewController:newDetailsVC animated:YES];
}
上面的NewDetailsPresenter
便是新闻概况页模块的Presenter,NewDetailsVC
便是新闻概况页模块的View。
在NewDetailsPresenter
中:
@implementation NewDetailsPresenter
/// 初始化View
- (instancetype)initWithID:(NSString *)idStr {
self = [super init];
if (self) {
self.idStr = idStr;
self.model = [[NewDetailsModel alloc] init];
self.model.delegate = self;
}
return self;
}
NewDetailsModel
天然也是新闻概况页模块的Model,设置署理的操作和首页相同。
在NewDetailsVC
中,与Presenter 是这样进行绑定的:
@implementation NewDetailsVC
- (instancetype)initWithPresenter:(NewDetailsPresenter *)presenter {
self = [super init];
if (self) {
self.presenter = presenter;
self.presenter.view = self;
}
return self;
}
其他的交互操作,数据传递,和之前说到的首页新闻列表相似。
上面的代码也能够看到,Presenter应该只关怀事务逻辑的完结,不直接操作View和进行界面跳转等视图层操作。
5 总结
MVP 的优点:
- 代码愈加模块化,易于保护和扩展。
- 视图和模型之间的交互逻辑通过Presenter 进行和谐,逻辑愈加明晰。
- Presenter 能够通过接口来完结,使得测验愈加简单。
但一起,MVP 形式也有不尽人意的地方:
- Presenter 层会添加代码量,添加了开发时刻和本钱。
- 关于小型运用程序来说,MVP 形式或许会显得过于繁琐。
- Presenter 和View 之间的接口规划或许会变得复杂,需求额外的注意。
6 项目Demo
MVP_ZhihuDaily