MVVM 和 MVC 的構建方式很類似,甚至能夠說在同一個項目中同時使用這兩種架構都不會有任何違和感。MVVM 能夠看做是 MVC 的衍生版,其承擔 MVC 架構下的 Controller 的一部分職責,這部分職責也就是 ViewModel 所須要作的事情。在 MVVM 中 Model 和 View 之間的通訊,是經過 ViewModel 構建的一條數據管道,ViewModel 將 View 所要展現的 Model 層的數據,轉化爲最終所須要的版本,View 直接來拿展現。固然這種管道的構建最好經過響應式框架: ReactiveObjc、RxSwift。 一樣,兩種架構一樣都是 Controller 充分了解程序各組件,並將他們構建和鏈接起來。但相比起 MVC ,MVVM 有如下幾點不一樣:git
本文不會使用響應式框架構建綁定關係,而是經過原生API:KVO+KVC 的方式構建。github
衆所周知,在 MVVM 架構中,Controller 是需持有 ViewModel 的。因此構建基類,創建一個 ViewModel 屬性是很是有必要的。這樣全部繼承自 基類 Controller 的子控制器都會擁有 ViewModel 。架構
@interface MVVMGenericsController<ViewModelType: id<ViewModelProtocol>> : UIViewController
@property (nonatomic, strong) ViewModelType viewModel;
@end
複製代碼
首先,基類 Controller 是泛型的(鑑於 Objective-C 中泛型的功能不想 Swift 那麼強大,這裏僅僅起到個標記的做用,幫助編譯器推斷 ViewModel 類型),暫且叫它 MVVMGenericsController ,其 ViewModel 類型須要實現 ViewModelProtocol 協議,暫且忽略這個協議,目前來講,不會對閱讀代碼產生任何影響。其次,定義了 viewModel 屬性。app
上文說到,MVVM 的關鍵在於構建 ViewModel 和 View 之間的管道,創建綁定關係。既然這樣,能夠在 Controller 中設定一個自動回調方法,在某個時機將其觸發並在方法當中構建綁定關係。框架
- (void)bind:(id<ViewModelProtocol>)viewModel { }
複製代碼
那麼,在什麼時候觸發這個方法呢?在觸發 bind:
方法以前,須要肯定 View 和 ViewModel 都不爲空(這裏的 View 指代,須要顯示數據的控件,如 Controller 中的 UITableView,ViewModelProtocol 協議後面會講到),由於須要在這個方法中創建綁定關係,因此必須保證兩者是有值的。通常來講,控制器中子控件的建立,是放在 - (void)viewDidLoad
或者 - (void)loadView
方法裏面,因此能夠在這兩個方法以後調用的 - (void)viewWillAppear:(BOOL)animated
響應 bind:
方法。固然,在每一個控制器中都去手動添加 [self bind]
這樣的代碼,無疑很麻煩。能夠經過 iOS 黑魔法:hook 操做實現自動調用。dom
@implementation UIViewController (Binding)
+ (void)load {
[self hookOrigInstanceMenthod:@selector(viewWillAppear:) newInstanceMenthod:@selector(mvvm_viewWillAppear:)];
}
- (void)mvvm_viewWillAppear:(BOOL)animated {
[self mvvm_viewWillAppear:animated];
if (!self.isAlreadyBind) {
if ([self isKindOfClass:[MVVMGenericsController class]]) {
objc_msgSend((MVVMGenericsController *)self, @selector(bindTransfrom));
}
self.isAlreadyBind = YES;
}
}
- (void)setIsAlreadyBind:(BOOL)isAlreadyBind {
objc_setAssociatedObject(self, &kIsAlreadyBind, @(isAlreadyBind), OBJC_ASSOCIATION_ASSIGN);
}
- (BOOL)isAlreadyBind {
return !(objc_getAssociatedObject(self, &kIsAlreadyBind) == nil);
}
- (void)bindTransfrom {}
@end
複製代碼
首先, hook 操做是在擴展當中實現的。在 + (void)load
方法當中將自定義的方法和系統的 viewWillAppear:
交換。+ (void)load
是在程序編譯加載階段由系統調用,而且只會調用一次,而且在 main 函數以前。故這裏是部署 hook 最理想的地方。其次,在這個擴展當中關聯了 isAlreadyBind 屬性,目的使一個 Controller 在銷燬以前只觸發一次 bind:
方法。再次,經過 isKindOfClass
判斷當前類是否是 MVVMGenericsController
的子類,若是是,就發送 bindTransfrom
消息,bindTransfrom
僅僅是個空方法,不出意外,永遠不會調用到這裏,它僅僅是讓編譯器不出現讓人厭煩的黃色警告。mvvm
在 MVVMGenericsController 纔是實現 bindTransfrom:
的地方,由於它纔是被真正發出的消息。函數
@implementation MVVMGenericsController
- (void)bindTransfrom {
if ([self conformsToProtocol:@protocol(ViewBinder)] && [self respondsToSelector:@selector(bind:)]) {
if ([self.viewModel conformsToProtocol:@protocol(ViewModelProtocol)]) {
[((id <ViewBinder>)self) bind:self.viewModel];
return;
}
}
}
@end
複製代碼
<ViewBinder>
協議提供了上文提到的,- (void)bind:(id<ViewModelProtocol>)viewModel
方法ui
@protocol ViewBinder <NSObject>
- (void)bind:(id<ViewModelProtocol>)viewModel;
@end
複製代碼
首先會判斷當前控制器是否實現了 ViewBinder
協議而且是否能響應 bind:
方法,若是能則派發 bind:
,參數是 ViewModel。ViewModel 的賦值是在控制器的自定義構造方法中,或者在 - (void)viewWillAppear:
以前。一旦沒有在合適的位置賦值,這裏會是 nil 。atom
這裏的綁定功能是響應式的,經過觀察屬性的改變當即獲得反饋。固然,經過代理也能夠實現,但響應式無疑是最輕量級的。在這裏是藉助 KVOController + 系統原生API KVC 實現的。一個對象的某個屬性被觀察後,一旦它發生值的改變,當即將它的結果經過 KVC 賦值給另外一個對象的某一個屬性,這便是創建綁定的過程。這裏給 NSObject 擴展一些方法:
@implementation NSObject (Binder)
- (void)bind:(NSString *)sourceKeyPath to:(id)target at:(NSString *)targetKeyPath {
[self.KVOController observe:self keyPath:sourceKeyPath options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
id newValue = change[NSKeyValueChangeNewKey];
if ([self verification:newValue]) {
[target setValue:newValue forKey:targetKeyPath];
}
}];
}
- (BOOL)verification:(id)newValue {
if ([newValue isEqual: [NSNull null]]) {
return NO;
}
return YES;
}
@end
複製代碼
sourceKeyPath
: 被觀察對象屬性的 keyPath、target
: 目標對象,即被觀察到的值賦值給的對象、at
:目標對象的屬性 keyPath。在 Objective-C 中沒有沒有像 Swift 當中的 \Foo.bar
的 KeyPath 功能,因此這裏的鍵路徑只能是字符串。
毫無疑問,ViewModel 是 MVVM 的核心部件。一個複雜功能的模塊,ViewModel 可能會有很大篇幅的代碼。ViewModel 應包含一個功能模塊的大部分業務邏輯,一個具備交互功能的頁面,無疑須要狀態的支持。因此 ViewModel 將數據加工好後經過 State 拋出給外部。另外一部分,外部經過 Action 通知 ViewModel 須要作的事情。
因此,一個 ViewModel 主要由兩部分組成 Action 和 State :
@interface DemoViewModel : NSObject<ViewModelProtocol> // 只是個空協議
// Action
- (void)changeTitle;
// State
@property (nonatomic, copy, readonly) NSString *title;
// Model
@property (nonatomic, copy, readonly) NSArray *titleArray;
@end
複製代碼
注意:這裏的 title(也就是 State )是 readonly
的,要嚴格採用這種方式,由於一個 State 僅僅是 只讀 的就夠了。
@interface DemoViewModel()
@property (nonatomic, copy, readwrite) NSString *title;
@end
@implementation DemoViewModel
- (instancetype)init {
self = [super init];
if (self) {
_titleArray = @[@"MVC", @"MVVM", @"SWift", @"ReactNative"];
_title = _titleArray[1];
}
return self;
}
- (void)changeTitle {
self.title = _titleArray[[self randomFloatBetween:0 andLargerFloat:4]];
}
@end
複製代碼
在 ViewModel 的實現部分中將 title 重置爲 readwrite ,由於要經過 changeTitle(也就是 Action)改變 title 的值。
Controller 的職責是將各組件鏈接起來,在這裏構建起 View <-> ViewModel 的管道。
@interface DemoViewController : MVVMGenericsController<DemoViewModel *><ViewBinder>
@end
複製代碼
首先,將 MVVMGenericsController 做爲父類,因 MVVMGenericsController 中定義了泛型 ViewModelType ,在這裏須要指定 ViewModel 的具體類型 <DemoViewModel *>
。其次,實現了 <ViewBinder>
協議,該協議提供 - (void)bind:(DemoViewModel *)viewModel
方法。
@interface DemoViewController ()
@property (nonatomic, strong) UILabel *titleLabel;
@end
@implementation DemoViewController
- (void)bind:(DemoViewModel *)viewModel {
[viewModel bind:@"title" to:self.titleLabel at:@"text"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.viewModel changeTitle];
}
複製代碼
在 - (void)bind:(DemoViewModel *)viewModel
方法中,創建了 ViewModel
的 title
同 titleLabel
的 text
的綁定關係,在這裏真正將 ViewModel 同 View 的管道打通。
在 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
方法中,調用了 ViewModel 的 - (void)changeTitle
方法,目的是改變 title
的值,而一旦 title
值改變,bind:
方法就會監聽到值的改變而且將 新的值 賦值給 titleLabel.text。這樣就造成了一個單向的數據信息流動。以下圖:
到此爲止,一個簡單的 MVVM 搭建完畢。固然,能夠有不少的 State 也能夠有不少的 Action 。只要遵照這個規則,一個 響應式 、單向數據流 的應用就誕生了。
很不幸的說,[viewModel bind:@"title" to:self.titleLabel at:@"text"];
這段代碼會產生一個引用循環:viewModel 經過 KVO 觀察了本身的 title 屬性。這樣 KVOController 沒法自動移除觀察者,因此要手動移除,固然,這個過程是在背後操做的:
const void* const kIsCallPop = &kIsCallPop;
@implementation UIViewController (RetainCircle)
+ (void)load {
[self hookOrigInstanceMenthod:@selector(viewDidDisappear:) newInstanceMenthod:@selector(mvvm_viewDidDisappear:)];
}
- (void)mvvm_viewDidDisappear:(BOOL)animated {
[self mvvm_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kIsCallPop) boolValue]) {
if ([self isKindOfClass:[MVVMGenericsController class]] && [((MVVMGenericsController *)self).viewModel conformsToProtocol:@protocol(ViewModelProtocol)]) {
NSObject *vm = ((MVVMGenericsController *)self).viewModel;
[vm.KVOController unobserveAll];
}
}
}
@end
@implementation UINavigationController (RetainCircle)
+ (void)load {
[self hookOrigInstanceMenthod:@selector(popViewControllerAnimated:) newInstanceMenthod:@selector(mvvm_popViewControllerAnimated:)];
}
- (UIViewController *)mvvm_popViewControllerAnimated:(BOOL)animated {
UIViewController* popViewController = [self mvvm_popViewControllerAnimated:animated];
objc_setAssociatedObject(popViewController, kIsCallPop, @(YES), OBJC_ASSOCIATION_RETAIN);
return popViewController;
}
複製代碼
一樣是經過方法交換,很簡單,代碼不解釋了。
經過閱讀這篇文章,對 MVVM 是否有了一個全新的認識呢?固然這套代碼還有不少不完善的地方,但不影響閱讀,不影響對代碼的理解。我想這樣就夠了。
就是這些,這裏是 Demo。