關(guān)于MVC和MVVM在項目實踐中的理解和總結(jié)的一些經(jīng)驗
首先來轉(zhuǎn)載一些MVC和MVVM的一些概念,網(wǎng)上一大堆,就不在這里論述了
MVC:
模型對象
模型對象封裝了應(yīng)用程序的數(shù)據(jù),并定義操控和處理該數(shù)據(jù)的邏輯和運算。例如,模型對象可能是表示游戲中的角色或地址簿中的聯(lián)系人。用戶在視圖層中所進(jìn)行的創(chuàng)建或修改數(shù)據(jù)的操作,通過控制器對象傳達(dá)出去,最終會創(chuàng)建或更新模型對象。模型對象更改時(例如通過網(wǎng)絡(luò)連接接收到新數(shù)據(jù)),它通知控制器對象,控制器對象更新相應(yīng)的視圖對象。
視圖對象
視圖對象是應(yīng)用程序中用戶可以看見的對象。視圖對象知道如何將自己繪制出來,并可能對用戶的操作作出響應(yīng)。視圖對象的主要目的,就是顯示來自應(yīng)用程序模型對象的數(shù)據(jù),并使該數(shù)據(jù)可被編輯。盡管如此,在 MVC 應(yīng)用程序中,視圖對象通常與模型對象分離。
在iOS應(yīng)用程序開發(fā)中,所有的控件、窗口等都繼承自 UIView,對應(yīng)MVC中的V。UIView及其子類主要負(fù)責(zé)UI的實現(xiàn),而UIView所產(chǎn)生的事件都可以采用委托的方式,交給UIViewController實現(xiàn)。
控制器對象
在應(yīng)用程序的一個或多個視圖對象和一個或多個模型對象之間,控制器對象充當(dāng)媒介??刂破鲗ο笠虼耸峭焦艿莱绦?,通過它,視圖對象了解模型對象的更改,反之亦然??刂破鲗ο筮€可以為應(yīng)用程序執(zhí)行設(shè)置和協(xié)調(diào)任務(wù),并管理其他對象的生命周期。
控制器對象解釋在視圖對象中進(jìn)行的用戶操作,并將新的或更改過的數(shù)據(jù)傳達(dá)給模型對象。模型對象更改時,一個控制器對象會將新的模型數(shù)據(jù)傳達(dá)給視圖對象,以便視圖對象可以顯示它。
https://liuzhichao.com/p/1379.html
MVVM
Model層
Model層是少不了的了,我們得有東西充當(dāng)DTO(數(shù)據(jù)傳輸對象),當(dāng)然,用字典也是可以的,編程么,要靈活一些。Model層是比較薄的一層,如果學(xué)過Java的小伙伴的話,對JavaBean應(yīng)該不陌生吧。
ViewModel層
ViewModel層,就是View和Model層的粘合劑,他是一個放置用戶輸入驗證邏輯,視圖顯示邏輯,發(fā)起網(wǎng)絡(luò)請求和其他各種各樣的代碼的極好的地方。說白了,就是把原來ViewController層的業(yè)務(wù)邏輯和頁面邏輯等剝離出來放到ViewModel層。
View層
View層,就是ViewController層,他的任務(wù)就是從ViewModel層獲取數(shù)據(jù),然后顯示。
http://www.cocoachina.com/ios/20150122/10987.html
ok,相信很多人都對MVC和MVVM模式有個大致的理解。
對我而言,MVVM解決了我一直非??鄲赖囊粋€問題,就是抽取viewController中的數(shù)據(jù)處理功能和網(wǎng)絡(luò)請求功能(突然想起一句話,設(shè)計模式并沒有幫我們徹底解決那一坨代碼,它只是將一坨代碼分成幾坨放在不同的地方而已)
然后看一下MVVM在實踐中是如何幫我優(yōu)雅地解決“抽取viewController中的數(shù)據(jù)處理功能和網(wǎng)絡(luò)請求功能,然代碼變得小清新”的問題吧。而想要方便地以MVVM模式開發(fā),利用ReactiveCocoa是不二之選,不然我相信你會block定義聲明滿天飛。
假設(shè)我有這樣一個頁面:GoodsListViewController,需要下拉刷新數(shù)據(jù),上拉加載更多。很簡單一個頁面。
GoodsListViewController.h
#import <UIKit/UIKit.h>
@interface GoodsListViewController : UIViewController<UICollectionViewDataSource,UICollectionViewDelegate,UICollectionViewDelegateFlowLayout>
@property (weak, nonatomic) IBOutlet UICollectionView *collectionView;
@property (nonatomic, strong) NSString *keyword;//為本頁接口請求數(shù)據(jù)時使用。
@property (nonatomic,strong) NSString *iSearchType;
@property (nonatomic,strong) NSString *iSortField;
@property (nonatomic,strong) NSString *iSortOrder;
@property (weak, nonatomic) IBOutlet UIButton *allBiiton;
@property (weak, nonatomic) IBOutlet UIButton *saleButton;
@property (weak, nonatomic) IBOutlet UIButton *priceButton;
@property (weak, nonatomic) IBOutlet UIButton *screeningButton;
@property (nonatomic , strong) NSString *goodsTypeStr;
@property (nonatomic, strong) NSString *goodsBrandID;
@property (nonatomic, strong) GoodsListVM *vm;
@end
GoodsListViewController.m
@interface GoodsListViewController ()<MJRefreshBaseViewDelegate>
@property (nonatomic, strong) MJRefreshFooterView *footer;
@property (nonatomic, strong) MJRefreshHeaderView *header;
@property (nonatomic, strong) GoodsListVC_RightBarItem *changeBtn;
@end
@implementation GoodsListViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self bind];
self.vm.iPageNo = 1; // viewController中只要這樣賦值就能實現(xiàn)數(shù)據(jù)獲取和處理,這樣使vc很清新,一切都交給viewModel處理.
[self setupRefresh];
}
- (void)bind {
@weakify(self);
[self.vm.requestBeforeObject subscribeNext:^(id x) {
@strongify(self);
[MBProgressHUD showHUDAddedTo:self.view animated:YES]; // 彈出菊花, 請求前回調(diào)
}];
[self.vm.successObject subscribeNext:^(id x) {
@strongify(self);
[MBProgressHUD hideHUDForView:self.view animated:YES]; // 隱藏菊花,成功回調(diào)
[self.header endRefreshing];
[self.footer endRefreshing];
[self.collectionView reloadData];
}];
[self.vm.errorObject subscribeNext:^(id x) {
@strongify(self);
[MBProgressHUD hideHUDForView:self.view animated:YES]; // 錯誤回調(diào),包括網(wǎng)絡(luò)錯誤和服務(wù)器返回數(shù)據(jù)錯誤
[ETPublic showHUDWithTitle:x andImage:nil andTime:1.0]; // 彈出菊花提示
[self.header endRefreshing];
[self.footer endRefreshing];
}];
}
#pragma mark - collectionView
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return 1;
}
-(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.vm.iGoodsListArr.count;
}
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
GoodsListsCell *cell = (GoodsListsCell*)[collectionView dequeueReusableCellWithReuseIdentifier:@"GoodsListsCellID" forIndexPath:indexPath];
cell.item1 = [self.vm.iGoodsListArr objectAtIndex:[indexPath row]];
return cell;
}
#pragma mark - 進(jìn)入刷新狀態(tài)就會調(diào)用
- (void)refreshViewBeginRefreshing:(MJRefreshBaseView *)refreshView {
if (_footer == refreshView) {
self.vm.iPageNo++;
}
else if (_header == refreshView) {
self.vm.iPageNo = 1;
}
}
#pragma mark - 設(shè)置刷新(項目用的是舊版MJRefresh)
- (void)setupRefresh {
_footer = [[MJRefreshFooterView alloc] init];
_footer.delegate = self;
_footer.scrollView = _collectionView;
_header = [[MJRefreshHeaderView alloc] init];
_header.delegate = self;
_header.scrollView = _collectionView;
}
- (void)dealloc {
[_footer free];
[_header free];
}
然后是viewModel層
#import <Foundation/Foundation.h>
@protocol ETViewModelDelegate <NSObject> // viewModel父類
@required
- (UIViewController *)returnThePushVC:(id)pushMsg; // 考慮到vc會push,這樣的邏輯我在vm做了,也可以不加
@end
@interface ETViewModel : NSObject<ETViewModelDelegate>
@property (nonatomic, strong) RACSubject *requestBeforeObject; // 調(diào)用請求前的回調(diào)
@property (nonatomic, strong) RACSubject *successObject; // 調(diào)用成功回調(diào)
@property (nonatomic, strong) RACSubject *failureObject; // 服務(wù)器數(shù)據(jù)錯誤回調(diào)
@property (nonatomic, strong) RACSubject *errorObject; // 網(wǎng)絡(luò)錯誤回調(diào),我的做法是不管哪一種都調(diào)用用這個回調(diào)
@property (nonatomic, strong) NSString *vcTitle;
@property (nonatomic, strong) NSString *vcLeftItemImage;
@end
GoodsListVM.h
#import "ETViewModel.h"
@interface GoodsListVM : ETViewModel
@property (nonatomic) NSInteger iPageSize;
@property (nonatomic) NSInteger iPageNo;
@property (nonatomic, strong) RACSubject *selectedSignal;
@property (strong, nonatomic) getGoodsListsResult *iResult;
@property (strong, nonatomic) NSMutableArray *iGoodsListArr;
@end
GoodsListVM.m
#import "GoodsListVM.h"
@implementation GoodsListVM
- (instancetype)init
{
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize {
[[RACObserve(self, iPageNo) filter:^BOOL(NSNumber *value) {
return value.integerValue!=0;
}] subscribeNext:^(id x) {
[self getGoodsList:[x integerValue]];
}];
self.vcTitle = @"商品列表";
self.iGoodsListArr = [[NSMutableArray alloc] init];
self.iPageSize = 10;
self.selectedSignal = [RACSubject subject];
}
- (void)getGoodsList:(NSInteger)page {
NSDictionary *_dic = @{@"searchType":self.iSearchType,@"keyword":self.keyword,@"sortField":self.iSortField,@"sortOrder":self.iSortOrder,@"specFilter":self.goodsTypeStr.length== 0?@"":self.goodsTypeStr,@"brandId":@"",@"pageSize":[NSNumber numberWithInteger:self.iPageSize],@"pageNo":@(page)}; // 組裝數(shù)據(jù)
[self.requestBeforeObject sendNext:nil]; // 發(fā)起回調(diào),一般用來調(diào)起等待菊花
@weakify(self);
[[ETAFNetWorkingClient sharedClient] POST:GOODS_LIST parameters:_dic success:^(NSURLSessionDataTask *task, id responseObject) {
@strongify(self);
if (0 == [[responseObject valueForKey:@"result"]integerValue]) {
[self.errorObject sendNext:@"沒有找到符合的商品"]; // 發(fā)起失敗回調(diào)
}else{
self.iResult = [[getGoodsListsResult alloc] initWithJson:responseObject];
if (page == 1) {
[self.iGoodsListArr removeAllObjects];
}
[self.iGoodsListArr addObjectsFromArray:self.iResult.iGoodsLists];
if (self.iResult.iGoodsLists.count == 0) {
// self.iPageNo--;
}
[self.successObject sendNext:nil]; // 發(fā)起成功回調(diào)
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[self.errorObject sendNext:@"網(wǎng)絡(luò)出問題了"]; // 發(fā)起失敗回調(diào)
}];
}
@end
這樣一個頁面就完成了,實現(xiàn)了將數(shù)據(jù)的獲取處理抽取了出來,并且在VC中只需要實現(xiàn)簡單的self.vm.iPageNo = 1或其他賦值操作就能實現(xiàn)加載刷新數(shù)據(jù),是否很方便呢?(以上比較重要的步驟都注釋了)
(可以到這里下載Demo )