最近在讀App架構方面的書。對這個感興趣是由於我意識到:程序員
而後onevcat的 關於 MVC 的一個常見的誤用一文也啓發了我,解決了一直以來我對Model的困惑,因此想用本身的例子再記錄一下,正好實現一個OC的版本。數組
在onevcat的文章中,大神貼出了一個標準的MVC結構圖。這個圖源自斯坦福CS193p的iOS應用開發課程。我在自學入門iOS的時候也學習了這個課程,不過是很老的版本,還用的Objective-C做爲教學語言。bash
當時我仍是個超級新手,看到這個圖的時候,最早懂的是View和Controller的交互,畢竟iOS開發最早接觸學習的確定是視圖的建立和交互。而看到Model和Controller的交互,只知道用通知來和Controller通訊,至於具體怎麼實現則是我一直的困惑。網絡
別看這只是一個單純的類之間的通訊問題,我相信不少缺少和周圍交流的新手開發者們對模型的使用很容易停留在「瘦Model」上。即使想實現上圖的標準用法,但一時半會兒還真很差找學習資料,反正不用實現圖片裏的標準,App同樣能開發,更沒動力找了。架構
所以很容易出現的狀況:Massive ViewController,把邏輯都堆在ViewController這個視圖容器裏,造成了龐大的、難以維護的單個類。mvc
這也是onevcat大神在他的文章中提出的兩個問題,Massive ViewController:app
用個人例子舉例說明這兩個問題。如今咱們實現了下圖的一個服務列表。框架
這個個人需求和個人服務這兩個列表共用一個模型:post
@interface MyReleaseModel : JSONModel
// 公用
@property(nonatomic, copy) NSString<Optional> *type;
@property(nonatomic, copy) NSString<Optional> *ID;
@property(nonatomic, copy) NSString<Optional> *addtime;
@property(nonatomic, copy) NSString<Optional> *views;
// 個人需求
@property(nonatomic, copy) NSArray<Optional> *dem_img;
@property(nonatomic, copy) NSString<Optional> *dem_desc;
@property(nonatomic, copy) NSString<Optional> *dem_price;
// 個人服務
@property(nonatomic, copy) NSArray<Optional> *s_img;
@property(nonatomic, copy) NSString<Optional> *s_desc;
@property(nonatomic, copy) NSString<Optional> *s_price;
複製代碼
而後在ViewController裏,有兩個當前列表的數組,以後的刪除邏輯就須要操做它:學習
// 需求列表array
@property(nonatomic, strong) NSMutableArray *demandMutaArray;
// 服務列表array
@property(nonatomic, strong) NSMutableArray *serviceMutaArray;
複製代碼
點擊刪除按鈕的邏輯,需求和服務的刪除邏輯同樣,因此這裏列舉需求的刪除代碼(OC的代碼真的很不適合展現……):
// 點擊了個人需求 刪除按鈕
// 因爲在cell裏,因此獲取到當前cell
ReleasedServiceAndDemandTableViewCell *myDemandCell = (ReleasedServiceAndDemandTableViewCell *)[[[sender view] superview] superview];
// 再獲取當前行數
NSIndexPath *myDemandIndexPath = [weakSelf.demandTableView indexPathForCell:myDemandCell];
// 使用了JSONModel,因此數組裏的每一項都是一個JSONModel類型的數據
MyReleaseModel *myDemandModel = weakSelf.demandMutaArray[myDemandIndexPath.row];
// 網絡請求寫在Model類裏了,因此從Model發出刪除的網絡請求(隱去具體的參數)
[myDemandModel deleteItemNetworkWithxxx:myDemandModel.xxx withxxx:myDemandModel.xxx];
// 在viewController類裏對數組操做
[weakSelf.demandMutaArray removeObjectAtIndex:myDemandIndexPath.row];
// 調用系統框架裏列表的刪除API
[weakSelf.demandTableView deleteRowsAtIndexPaths:@[myDemandIndexPath] withRowAnimation:UITableViewRowAnimationLeft];
複製代碼
如今就是這麼一個經過列表展現數據,而後能進行刪除操做的狀況。那麼這有什麼問題呢?
表面上看似有一個MyReleaseModel類,但它實際上是「瘦Model」,只提供須要的屬性字段,真正起到Model做用的則是上面的demandMutaArray
和serviceMutaArray
兩個數組。
onevcat在他的文章中提出:
咱們難以從外界維護或者同步 items(注:這裏是
demandMutaArray
和serviceMutaArray
兩個數組) 的狀態,添加和刪除操做被「綁定」在了這個 View Controller 裏,若是你還想經過其餘 View Controller 維護待辦列表的話,就不得不考慮數據同步的問題 (咱們會在稍後看到幾個具體的這方面的例子);另外,這樣的設置致使 items 難以測試。你幾乎沒法爲添加/刪除/修改待辦列表進行 Model 層的測試。
若是點擊刪除按鈕的話,會是這樣一個流程:
demandMutaArray
和serviceMutaArray
兩個數組)這實質是操做UI,而後變動Model,但同時也變動了UI。但以前那個標準的MVC圖所倡導的數據流動應該是:
- UI 操做 -> 經由 View Controller 進行模型更新 -> 新的模型經由 View Controller 更新 UI -> 等待新的 UI 操做
而上面的例子則在經由 View Controller 進行模型更新
這一步變成經由 View Controller 進行模型更新以及 UI 操做
。onevcat大神的觀點是:「雖然看起來這是很不起眼的變動,可是會在項目複雜後帶來麻煩。」
在onevcat大神的文章裏,他列舉了兩個場景證實他的觀點,能夠去看一下。
整個改進過程就是把ViewController裏操做數據的那部分邏輯遷移到Model層,而後Model層使用通知Notification把必要的信息回傳給ViewController,後者根據信息作相應動做。
Model層是app的內容,它不依賴於(像UIKit那樣的)任何app框架。也就是說,程序員對model層有徹底的控制。Model層一般包括model對象(在錄音app中的例子是文件夾和錄音對象)和協調對象(好比咱們的app例子中的負責在磁盤上存儲數據的Store類型)。被存儲在磁盤上的那部分model咱們稱之爲文檔model(documentation model)。
若是model層能作到和應用框架分離,咱們就能夠徹底在app的範圍以外使用它。咱們能夠很容易地在另外的測試套件中運行它,或者用一個徹底不一樣的應用框架重寫新的view層。這個model層將可以用於Android,macOS或者Windows版本的app中。
——《App架構——使用Swift進行iOS架構》
Model主要使用觀察者模式:
觀察者模式是在MVC中維持model和view分離的關鍵。
這種方式的優勢在於,不論變動源自哪裏(好比,view事件、後臺任務或者網絡),咱們均可以確信UI是和model數據同步的。
並且在遇到變動請求時,model將有機會拒絕或者修改這個請求
——《App架構——使用Swift進行iOS架構》
@interface MyReleaseModel ()
@property(nonatomic, strong) NSMutableArray *demandMutaArray;
@property(nonatomic, strong) NSMutableArray *serviceMutaArray;
@end
複製代碼
而後咱們須要監視這兩個列表數組的變化,Swift有值類型的數組,有監視屬性,能夠很是方便地監視屬性的變化。在OC裏我就先用KVO代替了。
#pragma mark - KVO method
// 觀察回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSMutableArray *oldArray = (NSMutableArray *)[change valueForKey:@"old"];
NSMutableArray *newArray = (NSMutableArray *)[change valueForKey:@"new"];
if ([keyPath isEqualToString:@"demandMutaArray"]) {
// demandMutaArray
} else {
// serviceMutaArray
}
}
// 添加KVO
- (void)observePropertyChange {
[self.demandMutaArray addObserver:self forKeyPath:@"demandMutaArray" options:NSKeyValueObservingOptionNew context:nil];
[self.serviceMutaArray addObserver:self forKeyPath:@"serviceMutaArray" options:NSKeyValueObservingOptionNew context:nil];
}
// 移除KVO
- (void)removeObserverFromProperty {
[self.demandMutaArray removeObserver:self forKeyPath:@"demandMutaArray"];
[self.serviceMutaArray removeObserver:self forKeyPath:@"serviceMutaArray"];
}
複製代碼
在適當的地方調用添加KVO和移除KVO的方法。而後在觀察回調方法,也就是每次屬性變化的時候,咱們作一個新值和舊值的對比,再定義一個enum,根據對比結果返回enum的狀態。
typedef enum : NSUInteger {
addItem,
removeItem,
reload,
} ChangeBehavior;
+ (ChangeBehavior)differenceBetweenOld:(NSMutableArray *)old andNew:(NSMutableArray *)new {
NSSet *oldSet = [NSSet setWithArray:old];
NSSet *newSet = [NSSet setWithArray:new];
if ([oldSet isSubsetOfSet:newSet]) {
// 添加
// ...
return addItem;
} else if ([newSet isSubsetOfSet:oldSet]) {
// 刪除
// ...
return removeItem;
} else {
// 既添加 也刪除
// ...
return reload;
}
}
// 觀察回調
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSMutableArray *oldArray = (NSMutableArray *)[change valueForKey:@"old"];
NSMutableArray *newArray = (NSMutableArray *)[change valueForKey:@"new"];
if ([keyPath isEqualToString:@"demandMutaArray"]) {
// demandMutaArray
ChangeBehavior behavior = [self.class differenceBetweenOld:oldArray andNew:newArray];
[[NSNotificationCenter defaultCenter] postNotificationName:@"MyReleaseModelDemandDidChangedNotification" object:self userInfo:@{@"MyReleaseModelDemandDidChangedNotification": @(behavior)}];
} else {
// serviceMutaArray
// 同上
}
}
複製代碼
@property(nonatomic, assign) NSInteger demandPage;
@property(nonatomic, assign) NSInteger servicePage;
@property(nonatomic, assign) NSInteger demandCount;
@property(nonatomic, assign) NSInteger serviceCount;
- (void)addItem:(NSMutableArray *)itemArray;
- (void)removeAtIndex:(NSIndexPath *)indexPath;
- (MyReleaseModel *)itemAtIndex:(NSIndexPath *)indexPath;
複製代碼
貼上接口定義,實現代碼就不在此貼上了,在實現裏會改變demandMutaArray
或serviceMutaArray
,從而觸發KVO回調,再經過通知Notification把相應的Enum狀態返回給訂閱通知的ViewController類。
先在相應的ViewController裏實例化Model,懶加載方式:
- (MyReleaseModel *)demandModel {
if (_demandModel == nil) {
_demandModel = [MyReleaseModel sharedInstance];
}
return _demandModel;
}
- (MyReleaseModel *)serviceModel {
if (_serviceModel == nil) {
_serviceModel = [MyReleaseModel sharedInstance];
}
return _serviceModel;
}
複製代碼
訂閱通知,以及當Model改變時,Model通知ViewController來改變View:
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(demandOrServiceDidChange:) name:@"MyReleaseModelDidChangedNotification" object:nil];
}
- (void)demandOrServiceDidChange:(NSNotification *)notification {
if ([notification.name isEqualToString:@"MyReleaseModelDemandDidChangedNotification"]) {
// 需求列表
ChangeBehavior behaivor = (ChangeBehavior)notification.userInfo[@"MyReleaseModelDemandDidChangedNotification"];
switch (behaivor) {
case addItem:
// 給table添加相應的cell
break;
case removeItem:
// 刪除table相應的cell
break;
case reload:
// 刷新tableView
break;
default:
break;
}
} else {
// 服務列表
// ...
}
}
複製代碼
或者當View改變時,View經過ViewController改變Model:
- (void)tapGestureAction:(UITapGestureRecognizer *)sender {
NSInteger index = sender.view.tag;
if (index == 1) {
NSLog(@"點擊了個人需求 刪除按鈕");
ReleasedServiceAndDemandTableViewCell *myDemandCell = (ReleasedServiceAndDemandTableViewCell *)[[[sender view] superview] superview];
NSIndexPath *myDemandIndexPath = [weakSelf.demandTableView indexPathForCell:myDemandCell];
// 重點:改變Model
[self.demandModel removeAtIndex:myDemandIndexPath];
// ....
// ....
}
複製代碼
這樣,咱們就實現了MVC圖所倡導的這種單向數據流動:
- UI 操做 -> 經由 View Controller 進行模型更新 -> 新的模型經由 View Controller 更新 UI -> 等待新的 UI 操做
這樣的方式寫Model,真正的把Model從ViewController獨立了出來,也實現了單一職責原則——Model全權負責數據,也達成了單向數據流,使整個流程不雜亂。