IOS架构之-MVVM架构/MVVM-C架构
文章结构
1.MVX架构问题
1.1理解Model层
1.2 万恶的ViewController
1.3 View的复用性
2.什么是MVVM
2.1MVVM各层的职责
修改记录
- 将RWTFlickrSearch工程model层业务逻辑实例方法实现方式替换成类方法实现。
一、MVX架构问题
1.1理解Model层:
M层要完成对业务逻辑实现的封装,一般业务逻辑最多的是涉及到客户端和服务器之间的业务交互。M层里面要完成服务器之间交互以及本地缓存和数据库存储(COREDATA, SQLITE,其他)等所有业务实现的封装,并向外提供的成员方法供其他层使用,而不是简简单单的数据结构。
那么这层业务逻辑实现的方法应该放在哪?
目前看到的有两种做法:
- DataMannage类集合所有当前页Model层数据请求
- 下面工程中所使用的协议法,定义相关的接口函数,在相应的类中方法,方便进一步细化。
- 直接写在Model类中
|-- 属性
|-- 类方法
第三种方式目前没有看到有工程这么使用,建议还是使用上面两种方式。
方式一的工程文件结构:
看下APIMannager的具体实现:
工程地址:
MVX架构DEMO
APIMannager.h
typedef void(^NetworkCompletionHandler)(NSError *error, id result);
typedef enum : NSUInteger {
NetworkErrorNoData,
NetworkErrorNoMoreData
} NetworkError;
@interface UserAPIManager : NSObject
- (void)fetchUserInfoWithUserId:(NSUInteger)userId completionHandler:(NetworkCompletionHandler)completionHandler;
- (void)refreshUserDraftsWithUserId:(NSUInteger)userId completionHandler:(NetworkCompletionHandler)completionHandler;
- (void)loadModeUserDraftsWithUserId:(NSUInteger)userId completionHandler:(NetworkCompletionHandler)completionHandler;
- (void)deleteDraftWithDraftId:(NSUInteger)draftId completionHandler:(NetworkCompletionHandler)completionHandler;
- (void)refreshUserBlogsWithUserId:(NSUInteger)userId completionHandler:(NetworkCompletionHandler)completionHandler;
- (void)loadModeUserBlogsWithUserId:(NSUInteger)userId completionHandler:(NetworkCompletionHandler)completionHandler;
- (void)likeBlogWithBlogId:(NSUInteger)blogId completionHandler:(NetworkCompletionHandler)completionHandler;
原工程作者这里是把整个app的网罗请求都放在一个类里,不建议这么做,工程越大这里的方法便会越多,可以再细分细分成某个模块或者某个界面的数据逻辑,而不是整个app所有的数据请求比如:PageDetailAPIMannager。
第二种方式下面结合具体工程我们在讲。
1.2 万恶的ViewController:
1.在MVC架构中,因为ViewController 类中包含了self.view 导致很多新手会把View的初始化布局和C的逻辑都写在ViewController中,View层和C层划分不明确。
2.因为UIKIt 框架的限制,页面跳转时不得不依赖viewController。
1.3 View的复用性
我们经常在工程中看到类似的代码:
//TGHomeMessageModel.h
#import <UIKit/UIKit.h>
#import "TGHomeMessageModel.h"
@interface TGMessageCell : UITableViewCell
@property(nonatomic,strong)TGHomeMessageModel *cellModel;
@end
//TGHomeMessageModel.m
-(void)setCellModel:(TGHomeMessageModel *)cellModel{
_cellModel = cellModel;
_lableTitle.text = cellModel.title;
_lableSubtitle.text = cellModel.message;
_lableDate.text = cellModel.createTimeStr;
BOOL isReadState = [cellModel.isRead boolValue];
_bageView.hidden = isReadState;
}
因为View与Model的耦合导致View的复用时候不得不重新抽离出基类view,再继承实现不同的赋值逻辑。复用性降低。
建议将View中的控件赋值剥离出来,来提高View的服用性。
目前看到两种做法:
1.用一个专门的viewHelper类实现model与view的赋值。
2.使用抽象类定义 binddata方法,再在具体view实现binddata方法。
两种方法原理类似,都需要对View进行一定的封装,只是数据赋值的方式有点不同。
方式一:
#import "UserAPIManager.h"
@interface BlogTableViewCell : UITableViewCell
- (void)setTitle:(NSString *)title;
- (void)setSummary:(NSString *)summary;
- (void)setLikeState:(BOOL)isLiked;
- (void)setLikeCountText:(NSString *)likeCountText;
- (void)setShareCountText:(NSString *)shareCountText;
- (void)setDidLikeHandler:(void(^)())didLikeHandler;
@end
BlogCellHelper.h
#import "Blog.h"
@interface BlogCellHelper : NSObject
+ (instancetype)helperWithBlog:(Blog *)blog;
- (Blog *)blog;
- (BOOL)isLiked;
- (NSString *)blogTitleText;
- (NSString *)blogSummaryText;
- (NSString *)blogLikeCountText;
- (NSString *)blogShareCountText;
- (void)likeBlogWithBlogId:(NSUInteger)blogId completionHandler:(NetworkCompletionHandler)completionHandler;
@end
BlogTableViewController.m
#pragma mark - Utils
- (void)reloadTableViewWithBlogs:(NSArray *)blogs {
for (Blog *blog in blogs) {
[self.blogs addObject:[BlogCellHelper helperWithBlog:blog]];
}
[self.tableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BlogCellHelper *cellHelper = self.blogs[indexPath.row];
BlogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ReuseIdentifier];
cell.title = cellHelper.blogTitleText;
cell.summary = cellHelper.blogSummaryText;
cell.likeState = cellHelper.isLiked;
cell.likeCountText = cellHelper.blogLikeCountText;
cell.shareCountText = cellHelper.blogShareCountText;
//点赞的业务逻辑
__weak typeof(cell) weakCell = cell;
[cell setDidLikeHandler:^{
if (cellHelper.blog.isLiked) {
[self.tableView showToastWithText:@"你已经赞过它了~"];
} else {
[[UserAPIManager new] likeBlogWithBlogId:cellHelper.blog.blogId completionHandler:^(NSError *error, id result) {
if (error) {
[self.tableView showToastWithText:error.domain];
} else {
cellHelper.blog.likeCount += 1;
cellHelper.blog.isLiked = YES;
//点赞的业务展示
weakCell.likeState = cellHelper.blog.isLiked;
weakCell.likeCountText = cellHelper.blogTitleText;
}
}];
}
}];
return cell;
}
BlogCellHelper类处理model数据,格式化后再赋值给cell视图。
方式二:
CEReactiveView.h
#import <Foundation/Foundation.h>
@protocol CEReactiveView <NSObject>
- (void)bindViewModel:(id)viewModel;
@end
RWTSearchResultsTableViewCell.m
- (void)bindViewModel:(id)viewModel{
RWTSearchResultsItemViewModel *photo = viewModel;
self.titleLabel.text = photo.title;
[self.imageThumbnailView sd_setImageWithURL:photo.url];
[RACObserve(photo, favourites) subscribeNext:^(NSNumber* _Nullable fav) {
self.favouritesLabel.text = fav.stringValue;
self.btnFavourites.hidden = (fav == nil);
}];
[RACObserve(photo, comments) subscribeNext:^(NSNumber* _Nullable com) {
self.commentsLabel.text = com.stringValue;
self.btnComment.hidden = (com == nil);
}];
photo.isVisible = YES;
[self.rac_prepareForReuseSignal subscribeNext:^(RACUnit * _Nullable x) {
photo.isVisible = NO;
}];
}
赋值
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
RWTSearchResultsItemViewModel *cellModel = self.viewModel.searchResults[indexPath.row];
RWTSearchResultsTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"RWTSearchResultsTableViewCell"];
if(cell == nil){
cell = [[NSBundle mainBundle]loadNibNamed:@"RWTSearchResultsTableViewCell" owner:nil options:nil].firstObject;
}
[cell bindViewModel:cellModel];
return cell;
}
与上面原理一致,同样需要对view进行一定的封装。好处是这样binddata就可以在view内部写赋值逻辑,减少viewController负担。要复用时,只要在继承类里面覆盖binddata方法就行。
这里指对view封装是指:一些复杂的view组件可能根据数据的type字段的不同类型,UI布局有显示有很大的变化时,就需要对view布局相关的方法进行一定的封装,给外部调用。
这样的方式会增加一部分工作量,但因为剥离了对具体数据的依赖,View的可复用性大大提高。
二、什么是MVVM
先弄张图来看下什么是MVVM:
MVC
MVP
MVVM
不管是MVC、MVP、MVVM有没有发现一些规律:
中心类:MVC、MVP、MVVM中心类分别是C /P/VM 。
中心类与View
不管是MVC、MVP、MVVM,View都是将用户事件传递给中心类, 中心类负责View层的数据更新。
而MVVM比MVP只是多了一层数据双向绑定,View根据相应的ViewModel自动变化。
中心类与Model
向model层调用业务逻辑实现方法请求数据,并根据需要更新model数据。
找到这层规律,我们再看MVVM就非常简单明了了。
2.1 MVVM各层的职责:
2.1.1 Model层
Model层各个MVX架构都相同:
1.提供业务逻辑实现方法(本地存储数据的交互、服务端数据的交互)
2.数据结构
2.1.2 View层(View/ViewController)
1.负责View的初始化释放与布局
2.响应用户交互事件
2.1.3 VIewModel层
- 从Model层 获取数据
- 同步数据到Model中
- 向View层提供接口,其中包括 数据接口(格式化的数据) 以及事件处理接口
- 通知View层数据改变。
- 业务逻辑
- 页面跳转
看到这些任务不少小伙伴肯定觉得似曾相识,这不就是MVC架构中C层需要负责的任务吗?是的,就是因为这样有人才提出MVVM实则就是把MVC架构中的C层的任务剥离出来放在了ViewModel层实现,于是从臃肿的C变成了臃肿的ViewModel,可复用性的C变成了可复用的性的ViewModel。
MVVM架构的缺点以及缺点的改进方法在下文我会跟大家细说。在这里这么讲,主要是帮助大家理解viewModel的职责,方便大家实际项目应用,大致相同与于我们之前写的MVC架构的C层。
2.2 结合工程我们抽几个关键代码来看下各层的实现:
工程目录结构
绿色部分圈出两个看起来有点奇怪的区域,等下我们再讲。先帖出几个关键性的代码:
Model层:
分两部分组成:
- 红色框: 业务逻辑实现
- 绿色框: 数据结构
业务逻辑实现
业务逻辑层部分: 由协议(RWTFlickrSearch)定义业务逻辑函数接口,实现类(RWTFlickrSearchImpl)实现具体的接口函数方式,定义Model层的业务逻辑层。
像下面这样:
RWTFlickrSearch协议
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Foundation/Foundation.h>
@protocol RWTFlickrSearch <NSObject>
+ (RACSignal *)flickrSearchSignal:(NSString *)searchString;
+ (RACSignal *)flickrImageMetadata:(NSString *)photoId;
@end
RWTFlickrSearchImpl
///RWTFlickrSearchImpl.h
#import <Foundation/Foundation.h>
#import "RWTFlickrSearch.h"
@interface RWTFlickrSearchImpl : NSObject<RWTFlickrSearch>
@end
///RWTFlickrSearchImpl.m
@implementation RWTFlickrSearchImpl
........
/// provides a signal that returns the result of a Flickr search
+ (RACSignal *)flickrSearchSignal:(NSString *)searchString{
return [[[RTWFlickrRequestMannage shareMannage] signalFromAPIMethod:@"flickr.photos.search"
arguments:@{@"text": searchString,
@"sort": @"interestingness-desc"}
transform:^id(NSDictionary *response) {
NSDictionary *photosDic = response[@"photos"];
NSArray *photoArray = photosDic[@"photo"];
NSNumber *totalNum = photosDic[@"total"];
RWTFlickrSearchResults *results = [RWTFlickrSearchResults new];
results.searchString = searchString;
results.totalResults = [totalNum integerValue];
NSArray *photos = [photoArray linq_select:^id(NSDictionary* jsonPhoto) {
RWTFlickrPhoto *photo = [RWTFlickrPhoto new];
photo.title = jsonPhoto[@"title"];
photo.photoID = jsonPhoto[@"id"];
photo.url = [[RTWFlickrRequestMannage shareMannage] photoSourceURLFromDictionary:jsonPhoto size:OFFlickrMediumSize];
return photo;
}];
results.photos = photos;
return results;
}]logAll];
}
+ (RACSignal *)flickrImageMetadata:(NSString *)photoId {
RACSignal *favourites = [[RTWFlickrRequestMannage shareMannage] signalFromAPIMethod:@"flickr.photos.getFavorites"
arguments:@{@"photo_id": photoId}
transform:^id(NSDictionary *response) {
NSString *total = [response valueForKeyPath:@"photo.total"];
return total;
}];
RACSignal *comments = [[RTWFlickrRequestMannage shareMannage] signalFromAPIMethod:@"flickr.photos.getInfo"
arguments:@{@"photo_id": photoId}
transform:^id(NSDictionary *response) {
NSString *total = [response valueForKeyPath:@"photo.comments._text"];
return total;
}];
return [[RACSignal combineLatest:@[favourites, comments] reduce:^id(NSString *favs, NSString *coms){
RWTFlickrPhotoMetadata *meta = [RWTFlickrPhotoMetadata new];
meta.comments = [coms integerValue];
meta.favourites = [favs integerValue];
return meta;
}] logAll];
}
这里用到了RWTFlickrSearch协议(抽象类的思想),具体函数实现由一个或者多个IMPL类实现,方便业务逻辑实现进一步拆分细化。
在ViewModel层你就可以看到类似这样的调用方式:
@interface RWTFlickrSearchViewModel ()
@property (weak, nonatomic) id<RWTViewModelServices> services;
@property NSMutableArray *mutablePreviousSearches;
@end
@implementation RWTFlickrSearchViewModel
...
- (RACSignal *)excuteSearchSignal{
[SVProgressHUD show];
return [[[RWTFlickrSearchImpl flickrSearchSignal:self.searchKey]
doNext:^(id _Nullable data) {
[SVProgressHUD dismiss];
[self.delegate respondsToSelector:@selector(searchCompleteWithResult:)] ? [self.delegate searchCompleteWithResult:data] : nil;
}]
doError:^(NSError * _Nonnull error) {
[SVProgressHUD showErrorWithStatus:error.localizedFailureReason];
}];
}
}
@end
直接调用类方法,来调用具体业务逻辑实现方法。
数据结构
#import <Foundation/Foundation.h>
@interface RWTFlickrPhoto : NSObject
@property (strong, nonatomic) NSString *title;
@property (strong, nonatomic) NSURL *url;
@property (strong, nonatomic) NSString *identifier;
@end
数据结构比较简单就不讲了。
View层
和我们平时写的MVC层没有什么区别,贴几个代表性的代码出来看下。
RWTFlickrSearchViewController.m
#import "RWTFlickrSearchViewController.h"
@interface RWTFlickrSearchViewController ()<UITableViewDelegate,UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITextField *textfiledSearch;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *loadingView;
@property (weak, nonatomic) IBOutlet UIButton *btnSearch;
@property (weak, nonatomic) IBOutlet UIButton *btnLogin;
@end
@implementation RWTFlickrSearchViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
[self setUpSubviews];
[self bindData];
}
- (void)setUpSubviews{
[self.loadingView startAnimating];
}
- (void)bindData{
RAC(self,title) = RACObserve(self.viewModel, navTitle);
RAC(self.viewModel,searchKey) = self.textfiledSearch.rac_textSignal;
RAC(self.loadingView,hidden) = [self.viewModel.searchCommand.executing not];
RAC(self.textfiledSearch,textColor) = RACObserve(self.viewModel, textColor);
self.btnSearch.rac_command = self.viewModel.searchCommand;
self.btnLogin.rac_command = self.viewModel.loginCommand;
[self.viewModel.errorSignal subscribeNext:^(NSError* _Nullable error) {
NSString *msg = error.localizedFailureReason;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:msg preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleDefault handler:nil];
[alert addAction:cancelAction];
[self presentViewController:alert animated:YES completion:nil];
}];
}
View层
- view视图布局
- view与ViewModel数据双向绑定
- 调用ViewModel提供的用户事件处理接口
比如这里的searchButton
的点击搜索事件,与具体的ViewModel事件接口executeSearch
相绑定,viewModel内部处理用户点击搜索的业务逻辑。
tip:
看下RACCommad的官方解释,按钮的点击事件会触发RACCommad任务的执行,同时也会将当前按钮的使能和RACCommand的canExecute
。
触发了事件的同时,又控制了按钮的使能防止重复触发一举两得。
ViewModel相关信息需要View层展示给用户的,也是在数据绑定这一步实现。比如这里的self.viewModel.errorSignal
信号。
errorSignal信号的初始化如下:
- (void)initialize{
......
RACSignal *errors = [RACSignal merge:@[self.loginCommand.errors,self.searchCommand.errors]];
self.errorSignal = errors;
}
ViewModel层
RWTFlickrSearchViewModel.h
#import <Foundation/Foundation.h>
@class RWTFlickrSearchResults;
@protocol RWTFlickrSearchViewModelDelegate <NSObject>
- (void)searchCompleteWithResult:(__kindof RWTFlickrSearchResults* _Nullable)result;
- (void)searchNeedLogin;
@end
@interface RWTFlickrSearchViewModel : NSObject
//view数据显示
@property (nonatomic,copy) NSString *navTitle;
@property (nonatomic,copy) NSString *searchKey;
@property (nonatomic,strong) UIColor *textColor;
//用户事件
@property (nonatomic,strong) RACCommand *searchCommand;
@property (nonatomic,strong) RACCommand *loginCommand;
//错误信息显示
@property (nonatomic,strong) RACSignal *errorSignal;
@property (nonatomic,weak) id<RWTFlickrSearchViewModelDelegate>delegate;
@end
提供了View需要的数据接口,以及用户事件响应接口。
RWTFlickrSearchViewModel.m
#import "RWTFlickrSearchViewModel.h"
#import "RWTFlickrSearchImpl.h"
@interface RWTFlickrSearchViewModel()
@end
@implementation RWTFlickrSearchViewModel
- (instancetype)init{
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize{
self.navTitle = @"search";
RACSignal *searchEnableSignal =
[[[RACObserve(self, searchKey)
map:^id _Nullable(NSString* _Nullable text) {
return @(text.length > 1);
}]
skip:1]
distinctUntilChanged];
[searchEnableSignal subscribeNext:^(NSNumber* _Nullable valid) {
UIColor *textColor = [valid boolValue] ? [UIColor blackColor] : [UIColor redColor];
self.textColor = textColor;
}];
self.searchCommand =
[[RACCommand alloc]initWithEnabled:searchEnableSignal
signalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [self excuteSearchSignal];
}];
self.loginCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
[self.delegate respondsToSelector:@selector(searchNeedLogin)] ? [self.delegate searchNeedLogin] : nil;
return [RACSignal empty];
}];
RACSignal *errors = [RACSignal merge:@[self.loginCommand.errors,self.searchCommand.errors]];
self.errorSignal = errors;
}
- (RACSignal *)excuteSearchSignal{
[SVProgressHUD show];
return [[[RWTFlickrSearchImpl flickrSearchSignal:self.searchKey]
doNext:^(id _Nullable data) {
[SVProgressHUD dismiss];
[self.delegate respondsToSelector:@selector(searchCompleteWithResult:)] ? [self.delegate searchCompleteWithResult:data] : nil;
}]
doError:^(NSError * _Nonnull error) {
[SVProgressHUD showErrorWithStatus:error.localizedFailureReason];
}];
}
因为这里用于搜索页面,所以拥有没有具体的数据Model。需要了解的小伙伴可以看下统一工程目录下的RWTSearchResultsItemViewModel
#import "RWTSearchResultsItemViewModel.h"
#import "RWTFlickrPhotoMetadata.h"
#import "RWTFlickrSearchImpl.h"
@interface RWTSearchResultsItemViewModel()
@end
@implementation RWTSearchResultsItemViewModel
- (instancetype)initWithModel:(RWTFlickrPhoto *)photo{
self = [super init];
if (self) {
_photoID = photo.photoID;
_title = photo.title;
_url = photo.url;
_photo = photo;
[self initialize];
}
return self;
}
- (void)initialize{
RACSignal *visibleStateChanged = [RACObserve(self, isVisible) skip:1];
RACSignal *visibleSignal = [visibleStateChanged filter:^BOOL(NSNumber* _Nullable value) {
return [value boolValue];
}];
RACSignal *hiddenSignal = [visibleStateChanged filter:^BOOL(id _Nullable value) {
return ![value boolValue];
}];
//从隐藏状态切换到出现1s后 请求数据
RACSignal *featchMetaData = [[visibleSignal delay:1.0f] takeUntil:hiddenSignal];
@weakify(self);
[featchMetaData subscribeNext:^(id _Nullable x) {
@strongify(self);
[[RWTFlickrSearchImpl flickrImageMetadata:self.photo.photoID] subscribeNext:^(RWTFlickrPhotoMetadata* _Nullable model) {
self.favourites = @(model.favourites);
self.comments = @(model.comments);
}];
}];
}
@end
基本上就是我上面讲的实现下面几个
- 从Model层 获取数据
- 同步数据到Model中(这里没有得到具体的体现)
- 向View层提供接口,其中包括 数据接口(格式化的数据) 以及事件处理接口
- 通知View层数据改变。
- 业务逻辑
- 页面跳转
页面跳转
这里是用coordinate的思想做的跳转,你也可以对Vc进行相关的弱引用再调用vc的方法跳转或者是RWTFlickrSearch工程中作者使用的抽象类的方式)
IOS架构之--使用Coordinator提高VC/ViewModel复用性
这一层大家有疑惑的可能主要是RAC的使用(RAC这个框架比较重,当实现MVVM主要是用的它的数据绑定功能,这块还是容易上手的,看两篇文章就行)。
好了,到这里一个完整的MVVM架构就讲完了。
原工程作者的博客:
MVVM Tutorial with ReactiveCocoa
工程地址:
RWTFlickrSearch
原作者在工程中用RAC实现了许多巧妙的用处,可以参考下原工程RAC的使用方法。
Reactive Cocoa文章专题
Reactive Cocoa不熟的小伙伴看一下下面几篇文章:
ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2
ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2
ReactiveCocoa进阶
三、MVVM问题以及改进方式
介于MVVM ViewModel担任了太多的任务,有人提出MVVM几个以下的缺点:
1.繁重的ViewModel代替了繁重的C
2.不可复用的ViewModel代替了不可复用的C
3.没有数据绑定工具的MVVM代码将会变得非常复杂,MVVM的实际重心在哪?如果是数据绑定那么数据绑定为什么在MVVM架构中没有体现。
优化改进方式:
- Daniel Hall提出可以通过将viewModel按照
Data Source
,Binding
,Responder
三类功能进一步细分,这么做可能会导致架构与传统的MVVM结构工程结构上看起来有点不一样,功能细化了一定程度上可以提高ViewModel的复用性。 - 使用MVVM-C架构,解耦ViewModel之间的跳转依赖,剥离ViewModel中的页面跳转逻辑,来提高ViewModel的复用性。
Daniel Hall的这篇文章:
The Problems with MVVM on iOS — Daniel Hall
工程地址:
参考文献:
深入分析MVC、MVP、MVVM、VIPER
MVVM Tutorial with ReactiveCocoa
iOS Architecture Patterns
A Better MVC
VIPER and Clean by Uncle Bob.
MVVM
How not to get desperate with MVVM implementation
Highly maintainable app architecture
MVVM is Not Very Good — Soroush Khanlou
The Problems with MVVM on iOS — Daniel Hall