iOS architecture design code example learning-MVP mode

Project Demo

The MVP architectural pattern is a software architectural pattern commonly used in iOS applications. It helps developers to separate business logic and user interface in the application, so that it is easier to manage and modify various parts of the application. In this article, I will introduce in detail the origin of the MVP architecture pattern, the core idea, the difference from the MVC pattern, and the functions and division of labor of each module.

This article will also use Zhihu Daily's App as a demo for learning the MVP model.

image.png

1 Origin

In the classic MVC mode that I have learned before, there are many advantages:

  • The code is clear, the responsibilities are clearly defined, and it is easy to maintain.
  • High code reusability, models and views can be reused in multiple controllers.

However, with the improvement of business, the amount of code and complexity also increase, and the shortcomings of the MVC model are also exposed:

  • Controllers tend to become too bloated , and it is difficult to separate logic code and business code .
  • Interaction logic between view and model is difficult to share and reuse across multiple controllers .
  • Difficult to unit test because controllers often depend on views and models.

To sum up, in the development of iOS applications, developers usually need to deal with a lot of business logic and interface codes, which are often mixed together and difficult to maintain. As iOS applications grow in complexity and size, better architectures are needed to manage this code.

The MVP architectural pattern was born to solve this problem. The MVP architectural pattern divides the application into three main parts: the model (Model), the view (View) and the presenter or called the model view coordinator (Presenter). This architectural pattern enables developers to better manage code and improve application maintainability and scalability.

2 brief introduction

image.png

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 层上直接可以使用的数据。

举个栗子:

image.png

如果从网络上请求下来的原始数据是“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层是由UIViewControllerUIView共同组成;
  • 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的fetchLatestNewsfetchBeforeNewsWithDate:(NSString *)date方法,会实现didReceiveLatestNews:(DayModel *)latestDayModeldidReceiveBeforeNews:(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,fetchLatestNewsDatafetchBeforeNewsData:(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 的代理,那么在fetchLatestNewsDatafetchBeforeNewsData:(NSString *)date方法中,Presenter 与Model 进行了交互,在上面介绍Model 的时候,我们也提到了Model 里面的fetchLatestNewsfetchBeforeNewsWithDate:方法,在这两个方法中,我们请求到了网络数据,并且交给了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 的通信。

Or give an example of Zhihu Daily project:

image.png image.png

Click any cell in the news list to enter the news details page. In this process, a news id number needs to be passed in to request news details. Then we can imagine that the View of the news list interface handles the click event, and the news id The ID number is passed to the Presenter of the news details page module, and then the Presenter makes a network request to get the data of the news details page and display it to the 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];
}
复制代码

The above NewDetailsPresenteris the Presenter of the news details page module, NewDetailsVCwhich is the View of the news details page module.

In 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;
}
复制代码

NewDetailsModelNaturally, it is also the model of the news details page module, and the operation of setting the proxy is the same as that of the home page.

In NewDetailsVC, the binding with the Presenter is as follows:

@implementation NewDetailsVC
- (instancetype)initWithPresenter:(NewDetailsPresenter *)presenter {
    self = [super init];
    if (self) {
        self.presenter = presenter;
        self.presenter.view = self;
    }
    return self;
}
复制代码

Other interactive operations and data transfer are similar to the homepage news list mentioned above.

It can also be seen from the above code that the Presenter should only care about the realization of business logic, and not directly operate the View and perform interface jumps and other view layer operations .

5 summary

Advantages of MVP:

  • The code is more modular and easier to maintain and extend.
  • The interaction logic between the view and the model is coordinated through the Presenter, making the logic clearer.
  • Presenter can be implemented through an interface , making testing easier.

But at the same time, the MVP model also has unsatisfactory places:

  • The Presenter layer increases the amount of code, increasing development time and cost.
  • For small applications, the MVP pattern can be overly cumbersome.
  • The interface design between Presenter and View can become complex and requires extra attention.

6 Project Demo

MVP_ZhihuDaily

Guess you like

Origin juejin.im/post/7225201975998693436