一直以來想寫一篇文章,可是沒找到合適的主題,前段時間一直在看 Flutter 的一些東西,原本有意向想寫關於 Flutter 的一些總結,可是看的有些零零散散,而且沒有實際應用過,因此也就擱置了。正好最近一段時間除主業務之餘,一直在作咱們 甘草醫生 用戶端的重構,恰好有一些對於 iOS 架構方面的見解與感悟,在這裏與你們分享。 萬事開頭難!其實在開始重構以前,我是很糾結的,一直很難開始。我也曾翻閱過不少資料,想找到一個合適的符合咱們本身目前業務的架構,最後作了種種的比對與測試,選擇了 MVVM + 組件化 + AOP
的模式來重構。可能有人會疑問,你爲何選擇這樣的架構模式?使用這些模式有什麼好處?這些抽象的模式概念具體應該怎麼在實際項目中運用?OK,那咱們就帶着這些疑問一步步往下看。html
咱們先來了解一下在 iOS 中經常使用的一些架構模式ios
MVC 關於 MVC(Model-View-Controller)這個設計模式我相信稍有些編程經驗的人都瞭解至少據說過,做爲應用最爲普遍的架構模式,你們應該都是耳熟能詳了,可是不一樣的人對 MVC 的理解是不一樣的。在 iOS 中,Cocoa Touch 框架使用的就是 MVC ,以下 git
這是蘋果典型的 MVC 模式,用戶經過 View 將交互(點擊、滑動等)通知給 Controller,Controller 收到通知後更新 Model,Model 狀態改變之後再通知 Controller 來改變他們負責的 View。因爲在 iOS 中咱們經常使用的 UIViewController 自己就自帶一個 View,因此在 iOS 開發中 Controller 層和 View 層老是緊密的耦合在一塊兒,若是一個頁面業務邏輯量大的話,一個視圖控制器常常會不少行的代碼,致使視圖控制器很是的臃腫。 可見,MVC 模式雖然能帶來簡單的業務分層,可是想必各位使用 MVC 模式的 iOSer 們常常會被如下幾個問題困擾MVVMgithub
MVVM (Model-View-ViewModel),其實也是基於 MVC 的。上面咱們說的 MVC 臃腫的問題,在 MVVM 的架構模式中獲得瞭解決,咱們一些經常使用的網絡請求、數據存儲等都交給它處理,這樣就能夠分離出 ViewController 裏面的一些代碼使其「減肥」。 編程
如圖,就是 MVC 到 MVVM 的演變過程,在 MVVM 中 V 包含 View 和 ViewController,能夠看出來 MVVM 其實就是把 MVC 中的 C 分離出來一個 ViewModel 用來作一些數據加工的事情。在上面 MVC 模式中講了,一個 ViewController 常常會有不少東西要處理,數據加工、網絡請求等,如今均可以交給 ViewModel 去作了。這樣,Controller 就能夠實現「減肥」,而更加專一於本身的數據調配的工做,綁定 ViewModel 和 View 的關係 能夠看出 MVVM 的模式解決了 MVC 模式中的一些問題,使得 ViewController 代碼量減小、使得可讀性變高、代碼單元測試變得簡單。可是 MVVM 也有其一些缺陷,好比因爲 ViewModel 和 View 的綁定,那麼出現了 bug 第一時間定位不到 bug 的位置,有多是 View 層的 bug 傳到了 Model 層。還有一點就是對於較大的工程的項目,數據的綁定和轉換須要較大的成本。關於其缺點以及可行的解決方式,在 Casa Taloyum 的 iOS應用架構談 網絡層設計方案 已經說明的比較詳細,有興趣的童鞋能夠去看一下,幾篇關於架構方面的文章都很值得一讀。其餘的一些架構模式swift
還有一些其餘的架構模式,好比 MVP(Model-View-Presenter)、VIPER(View-Interactor-Presenter-Entity-Routing)、MVCS(Model-View-Controller-Store)等,其實都是基於 MVC 思想派生出來的一些架構模式,基本都是爲了給 Controller 減負而生的,因此仍是那句話,萬變不離 MVC !設計模式
瞭解到每一個架構模式的優缺點以後,這裏,我決定用 MVVM 的架構模式來重構咱們的 APP。那麼說到 MVVM ,咱們就確定是要提到 RAC ,也就是 ReactiveCocoa,它是一個響應式編程的框架,可使每層交互起來更加方便清晰。固然, RAC 確定不是實現數據綁定的惟一方案,在 iOS 中好比 KVO、Notification、Delegate、Block等均可以實現,只不過是 RAC 的實現更加優雅一些,因此咱們常常會採用 RAC 來實現數據的綁定。關於 RAC ,下面一張圖很清晰的解釋了它的思想,也就是 FRP(Function Reactive Programming)函數響應式編程 瀏覽器
上圖能夠看到 c 根據 a 和 b 的值變化的過程。舉個例子,咱們通常在登陸的時候,會限制輸入手機號的長度,那麼按着以往的作法,就是實現 UITextField 的代理,監聽輸入文字的變化,以下//一、導入代理
<UITextFieldDelegate>
//二、設置代理
self.phoneTextField.delegate = self;
//三、實現代理
- (void)textFieldDidChange:(UITextField *)textField {
if (textField == self.phoneTextField) {
if (textField.text.length > 11) {
textField.text = [textField.text substringToIndex:11];
}
}
}
複製代碼
那麼若是使用 RAC ,以下bash
@weakify(self);
[[self.phoneTextField.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
return value.length > 11 ? [value substringToIndex:11] : value;
}] subscribeNext:^(NSString *x) {
@strongify(self);
self.phoneTextField.text = x;
}];
複製代碼
能夠看出代碼變得更加清晰了,咱們只須要實現對 phoneTextField
信號的監聽,就能夠實現了。咱們再來看一個例子,好比在咱們用戶端的登陸界面,以下圖 微信
//一、導入代理
<UITextFieldDelegate>
//二、設置代理
self.phoneTextField.delegate = self;
self.passwordTextField.delegate = self;
//三、實現代理
- (void)textFieldDidChange:(UITextField *)textField {
if (self.phoneTextField.text.length == 11 && [self.passwordTextField isNotBlank]) {
self.loginButton.enabled = YES;
} else {
self.loginButton.enabled = NO;
}
}
複製代碼
而使用 RAC 則以下
@weakify(self);
[[[RACSignal combineLatest:@[self.phoneTextField.rac_textSignal,
self.passwordTextField.rac_textSignal]] map:^id _Nullable(RACTuple * _Nullable value) {
RACTupleUnpack(NSString *phone, NSString *password) = value;
return @([password isNotBlank] && phone.length == 11);
}] subscribeNext:^(NSNumber *x) {
@strongify(self);
if (x.boolValue) {
self.loginButton.enabled = YES;
} else{
self.loginButton.enabled = NO;
}
}];
複製代碼
咱們將 self.phoneTextField.rac_textSignal
和 self.passwordTextField.rac_textSignal
這兩個信號合併成一個信號而且監聽,實現必定的邏輯,簡單明瞭。固然, RAC 的好處遠遠不止這些,這裏只是冰山一角,有興趣的能夠去本身用一用這個庫,體驗更多的功能,這裏也就很少贅述了。
組件化這個概念相信你們都據說過,使用組件化的好處就是使咱們項目更好的解耦,下降各個分層之間的耦合度,使項目始終保持着 高聚合,低耦合
的特色。舉個簡單的例子,在 iOS 中頁面之間的跳轉,兩個開發人員負責開發兩個頁面,小 A 負責開發的 AViewController 已經開發完畢,而後須要點擊按鈕跳到小 B 負責的 BViewController,而且須要傳一個值,以下
//一、導入BViewController
#import "BViewController"
//二、跳轉
BViewController *bViewController = [[BViewController alloc]init];
bViewController.uid = @"123";
[self.navigationController pushViewController:bViewController animated:YES];
複製代碼
這時候小 A 已經準備去寫其餘業務了,可是一問才發現小 B 並無開始寫 BViewController,還須要一段時間才能寫,那麼小 A 就鬱悶了,要麼就等着小 B 寫完我再去作其餘的,要麼就先註釋我這段代碼,等到小 B 寫完我再解註釋。形成這種狀況的緣由就是由於兩個頁面之間牢牢地耦合在一塊兒了,在開發人員少或者獨立開發的狀況下咱們常用這種方式進行頁面間的跳轉和傳值,頁面基本都是一我的負責,因此感受不到問題,試想一下在幾十人開發的工做組中,劃分很細的狀況下,你本身的脫節是否是給別人帶去了沒必要要的麻煩。我相信這是全部人都不想發生的,那麼咱們就須要對頁面進行組件化解耦,這裏我所使用的組件化方案是 target-action
方式,使用的是 Casa Taloyum 的 CTMediator,其主要的思想就是經過一箇中間者來提供服務,經過 runtime
來調用組件服務,好比之前的依賴關係以下
//一、導入Mediator
#import "CTMediator+BViewControllerActions.h"
//二、跳轉
UIViewController *viewController = [[CTMediator sharedInstance] gc_bViewController:@{@"uid": @"123"}];
[self.navigationController pushViewController:viewController animated:YES complete:nil];
複製代碼
這樣小 A 就不用管小 B 是否是寫完沒,也不須要導入小 B 的頁面,就能夠跳轉到小 B 的頁面,實現了頁面間的解耦。能達到這一目的的功臣就是咱們的中間者,咱們來看看它作了什麼,咱們仍是以咱們登陸頁面爲例,咱們從登錄跳轉到註冊頁面的代碼以下
//一、導入Mediator
#import "CTMediator+RegistViewControllerActions.h"
//二、跳轉
UIViewController *viewController = [[CTMediator sharedInstance] gc_registViewController];
[self.navigationController pushViewController:viewController animated:YES complete:nil];
複製代碼
其中 CTMediator+RegistViewControllerActions.h
中的代碼以下
//
// CTMediator+RegistViewControllerActions.m
// GCUser
//
// Created by HenryCheng on 2019/4/15.
// Copyright © 2019 HenryCheng. All rights reserved.
//
#import "CTMediator+RegistViewControllerActions.h"
NSString *const gc_targetRegistVC = @"RegistViewController";
NSString *const gc_actionRegistVC = @"registViewController";
@implementation CTMediator (RegistViewControllerActions)
- (UIViewController *)gc_registViewController {
UIViewController *viewController = [self performTarget:gc_targetRegistVC
action:gc_actionRegistVC
params:@{@"title": @"註冊"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
return viewController;
} else {
return [[UIViewController alloc] init];
}
}
@end
複製代碼
其中重要的就是 performTarget:action:params:shouldCacheTarget:
這個方法,內部的實現方式以下
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget {
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 這裏是處理無響應請求的地方之一,這個demo作得比較簡單,若是沒有能夠響應的target,就直接return了。實際開發過程當中是能夠事先給一個固定的target專門用於在這個時候頂上,而後處理這種請求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這裏是處理無響應請求的地方,若是無響應,則嘗試調用對應target的notFound方法統一處理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 這裏也是處理無響應請求的地方,在notFound都沒有的時候,這個demo是直接return了。實際開發過程當中,能夠用前面提到的固定的target頂上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
複製代碼
能夠看到若是有響應則調用 safePerformAction:target: params:
這個方法,以下
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params {
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
const char* retType = [methodSig methodReturnType];
if (strcmp(retType, @encode(void)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil;
}
if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSUInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
複製代碼
經過這兩個方法咱們就能夠看到整個 CTMediator 實現的思路了,爲何 AViewController 不引用 BViewController 還能向其進行跳轉傳值,原來都是因爲 runtime
在中間起做用。 固然,雖然中間者這個方案能很好地實現各頁面之間的解耦,可是也有它的缺點。咱們能夠看到咱們在 CTMediator+RegistViewControllerActions.h
中定義的 gc_targetRegistVC
和 gc_actionRegistVC
這兩個常量,分別對應 ‘target’ 和 ‘action’,這裏面須要注意的是必定要細心,若是這兒寫錯,會引起未知的錯誤,可是編譯器並不會提示,對應的 Target_...
必定要和這裏的 target
一致,不然就會引起錯誤。這種方案的實施對開發人員的細心程度是有很大要求的,由於若是有錯誤,在編譯中沒法發現的。 組件化的方案的實施還有不少其餘的方案,好比 url-block
、protocol-class
方式,有興趣的能夠看看蘑菇街的 MGJRouter,還有就是阿里的 BeeHive ,它是基於 Spring 的 Service 理念,使用 Protocol
的方式進行模塊間的解耦。
先看一個案例,小 C 最近愁眉苦臉,你發現了他狀態不對勁,因而就發生了下面的對話
你:「小 C,你這是怎麼啦,是否是工做上有什麼不順心的?」
小 C:「是啊,最近接到一個需求,讓我很頭疼!」
你:「接到需求不是很正常,作就是了啊!」
小 C:「你不知道,這個需求是統計每一個頁面的瀏覽狀況,就是用戶到了這個頁面我就要統計一下,
運營產品他們要看 PV,因而我就在基類裏面的 `viewDidLoad` 方法加了一下,這樣很簡單就解決了」
小 C:「但是他們又說還要我作每一個頁面按鈕的點擊統計,你說這 APP 幾百個頁面,這麼多按鈕,我怎麼加啊,
就算我加了,個人代碼也會由於這些與業務無關的代碼而變得混亂,萬一哪天不統計再讓我刪了,那我不是要命了啊!愁死我了!」
你:「那你這使用 AOP 就能夠了啊」
小 C :「A...OP???」
複製代碼
AOP(Aspect-oriented programming),面向切面編程,是計算機科學中的一種程序設計思想,旨在將橫切關注點與業務主體進行進一步分離,以提升程序代碼的模塊化程度。在 iOS 中有一個應用很是多的輕量級的 AOP 庫 Aspects ,它容許你能在任何一個類和實例的方法中插入新的代碼。看到這裏,你可能就已經知道小 C 的問題該如何解決了,下面是使用 Aspects 實現頁面統計的代碼
//
// GCViewControllerIntercepter.m
// GCUser
//
// Created by HenryCheng on 2019/4/25.
// Copyright © 2019 HenryCheng. All rights reserved.
//
#import "GCViewControllerIntercepter.h"
#import <Aspects/Aspects.h>
@implementation GCViewControllerIntercepter
+ (void)load {
[GCViewControllerIntercepter sharedInstance];
}
+ (GCViewControllerIntercepter *)sharedInstance {
static GCViewControllerIntercepter *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[GCViewControllerIntercepter alloc] init];
});
return _sharedClient;
}
- (instancetype)init {
if (self == [super init]) {
[UIViewController aspect_hookSelector:@selector(viewDidLoad)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo, UITouch *touch, UIEvent *event) {
if ([aspectInfo.instance isKindOfClass:[UIViewController class]]) {
// 頁面統計的代碼
}
} error:NULL];
[UIControl aspect_hookSelector:@selector(beginTrackingWithTouch:withEvent:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo, UITouch *touch, UIEvent *event) {
if ([aspectInfo.instance isKindOfClass:[UIButton class]]) {
// 按鈕統計的代碼
}
} error:NULL];
}
return self;
}
@end
複製代碼
咱們能夠看到,經過新建 GCViewControllerIntercepter
這個類就實現了頁面的統計和按鈕點擊統計功能,你只須要實現就行,連導入都不用,若是哪天你不須要這些統計的代碼了,你直接從項目中移除這個類就能夠了。是否是很簡單!這就是 AOP 的一個使用實例,經過 + (void)load
這個方法(+ load
做爲 Objective-C 中的一個方法,與其它方法有很大的不一樣。它只是一個在整個文件被加載到運行時,在 main 函數調用以前被 ObjC 運行時調用的鉤子方法),實現了 GCViewControllerIntercepter
這個類被調用,而後經過 Aspects 實現對 UIViewController 和 UIControl 的 hook。這樣在每一個頁面被加載、每一個按鈕被點擊以前這邊就能夠捕捉到。 還有就是有人提到過去基類,也就是拋棄厚重的 base ,直接使用 AOP ,這樣的話好比我想寫個新 demo 就不用引入各類父類了,直接 hook 拿來用就行了。這種方法我的以爲沒有到大工程的時候仍是用繼承來實現比較好。若是工程量比較大便於各個開發人員調試,可使用這種方法。 固然 AOP 的做用也不只如此,這裏就說這麼一個咱們經常使用的 hook 的例子,有興趣能夠下去好好了解下。
一、
AspectOptions
有四個值,分別是AspectPositionAfter
、AspectPositionInstead
、AspectPositionBefore
和AspectOptionAutomaticRemoval
,這樣你能夠決定你 hook 的位置二、對於
+ (void)load
還有+ (void)initialize
這兩個方法不是太瞭解的童鞋能夠看看大左 Draveness 的 你真的瞭解 load 方法麼? 和 懶惰的 initialize 方法 這兩篇文章,瞭解這兩個方法相信對你會頗有幫助
瞭解了上面的內容,接下來咱們看看在實際項目中的應用
項目的目錄結構
重構的項目結構如上圖,相信你們一看名稱就大概知道每一個文件夾是作什麼的,因爲Model
、View
、ViewController
和 ViewModel
這幾個類聯繫比較緊密,因此建議這幾個類的項目結構保持一致,以下圖Login
模塊裏面去尋找。並且建議目錄不要過深,通常三層就夠了,過深的話查找起來比較麻煩。 Category 的使用
可能你們已經看到了,個人項目目錄裏面有一項是 AppDelegate+Config
這一項,這其實就是 AppDelegate
的一個 Category
。在 iOS 開發中 Category
隨處可見,如何應用那就是看本身的需求狀況了,這裏我用 AppDelegate+Config
這個類來處理 AppDelegate
裏面的一些配置,減小 AppDelegate
的代碼,讓項目更加清晰,使用了之後咱們能夠看到 AppDelegate
目錄的代碼片斷
#import "AppDelegate.h"
#import "AppDelegate+Config.h"
#import "GCPushManager.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self configTabbar];
[self registWeChat];
[self configNetWork];
[self configJPushWithLaunchOptions:launchOptions];
[self configKeyboard];
[self configBaiduMobStat];
[self configShareSDKWithLaunchOptions:launchOptions];
return YES;
}
// between iOS 4.2 - iOS 9.0
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation {
[self handleOpenURL:url];
return NO;
}
// after iOS 9.0
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
[self handleOpenURL:url];
return NO;
}
複製代碼
這樣代碼看起來很清晰,相信你們都有過一打開 AppDelegate
這個類看到一大堆代碼,東找西找很不規範的經歷。因爲是項目重構初期,AppDelegate
和 AppDelegate+Config
使用的比較多,暫時先放在這裏,後期再將其移動到合適的位置。
CocoaPods 的使用
相信這個東西你們都用過,爲何要強調一下 CocoaPods 的使用,由於在我整理以前項目時發現,有的地方(好比微信支付、支付寶支付)就是直接將 lib 直接拖進工程,有的還須要各類配置,這樣若是升級或者移除的時候就很麻煩。使用 CocoaPods 管理的話那麼升級或者移除就很方便,因此建議仍是能使用 CocoaPods 安裝的就直接使用其安裝,最好不要直接在項目中添加第三方。 還有一種狀況就是有時候第三方知足不了咱們的需求,須要修改一下,因此有些就不集成在 CocoaPods 裏面了(萬一一不當心 update 之後修改的內容被覆蓋)。這裏我想說的是,對於這種狀況你仍然可使用 CocoaPods,那麼怎麼解決須要修改代碼的問題?沒錯,就是 Category !
MVVM的運用
具體項目的實現咱們仍是以登陸爲例,在 ViewModel 中
- (void)initialize {
[super initialize];
RAC(self, isLoginEnable) = [[RACSignal combineLatest:@[
RACObserve(self, phone),
RACObserve(self, password)
]] map:^id _Nullable(RACTuple * _Nullable value) {
RACTupleUnpack(NSString *phone, NSString *password) = value;
return @([phone isNotBlank] && [password isNotBlank] && phone.length == 11); }];
RAC(self.loginRequest, params) = [[RACSignal combineLatest:@[
RACObserve(self, phone),
RACObserve(self, password)
]] map:^id _Nullable(RACTuple * _Nullable value) {
RACTupleUnpack(NSString *phone, NSString *password) = value;
return @{@"phone": GC_NO_BLANK(phone),
@"pwd": GC_NO_BLANK(password)
}; }];
}
- (RACCommand *)loginCommand {
if (!_loginCommand) {
@weakify(self);
_loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
@strongify(self);
return [self.loginRequest requestSignal] ;
}];
}
return _loginCommand;
}
複製代碼
這裏咱們作了網絡的請求以及一些數據的綁定,在 ViewController 中
- (void)gc_bindViewModel {
[super gc_bindViewModel];
RAC(self.viewModel, phone) = self.loginView.phoneTextField.rac_textSignal;
RAC(self.viewModel, password) = self.loginView.passwordTextField.rac_textSignal;
RAC(self.loginView.loginButton, enabled) = RACObserve(self.viewModel, isLoginEnable);
@weakify(self);
[[[self.loginView.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside] throttle:0.25] subscribeNext:^(__kindof UIControl * _Nullable x) {
@strongify(self);
[self.viewModel.loginCommand execute:nil];
}];
[[self.viewModel.loginCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) {
if (x.boolValue) {
[GCHUDManager show];
} else {
[GCHUDManager dismiss];
}
}];
// 登陸命令監聽
[self.viewModel.loginCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *x) {
@strongify(self);
UserModel *userModel = [UserModel modelWithDictionary:x];
[[GCCacheManager sharedManager] updateDataWithDictionary:x key:GCUserInfoStoreKey()];
[GCPushManager gc_setAlias:x[@"phone"]];
if (userModel.is_agree.intValue == 0) {
// 未贊成甘草協議
} else if (userModel.is_agree.intValue == 1 && userModel.pwd_status.intValue == 0) {
// 贊成協議可是沒改過密碼
} else {
// 登陸
}
} error:^(NSError * _Nullable error) {
}];
}
複製代碼
能夠看到 ViewController 將 View 和 ViewModel 進行了綁定,而且當登陸按鈕點擊的時候監測登陸信號的變化,根據其信號執行的開始和結束來控制 HUD 的顯示和消失,而後再根據信號的返回結果來處理相關的登陸配置和跳轉(極光推送的登陸、根據狀態執行跳轉邏輯等)。這裏網絡的請求都是在 ViewModel 中進行的,ViewController 只負責處理ViewModel、View 和 Model 之間的關係。
DRY(Don't repeat yourself),能封裝起來的類必定要封裝起來,到時候使用也簡單,千萬不要爲了一時之快而各類 ctrl + c
和 ctrl + v
,這樣會使你的代碼混亂不堪,這其實也是項目臃腫的一個緣由。在重構的過程當中就封裝了不少的類,管理起來很方便
其實最開始的時候一直都有重構的想法,可是遲遲沒有動手。其中一個緣由就是不知道該如何動手,不知道該使用什麼工具,該使用哪一種方案。等到真正開始的時候發現其實沒有想象中的那麼難,因此當你有想法的時候你就去作,在作的過程當中你能夠慢慢體會。 在重構以前,我又從新讀了一下代碼規範,也就是 禪與 Objective-C 編程藝術 這本書,並在重構的過程當中嚴格執行,好比 loginButton
就毫不會寫成 loginBtn
,相信我,按着規範來,你會體會到其中的意義的。 在作一個 APP 以前,在咱們新建工程的時候,就應該已經肯定你的架構模式,而且在之後的業務處理中,嚴格的按着這種設計模式執行下去。若是在前面需求量很少的時候你還能按着最初的設計模式執行下去,在業務忽然增多的時候,爲了偷懶省事,直接各類代碼混亂的糅合在一塊兒,各類 ctrl + c
和ctrl + v
,致使架構的混亂引發蝴蝶效應,那麼這個架構在後期若是再想從新規範起來將會是個費時費力的過程。因此,在最初設計的時候咱們就應該肯定架構方案,以及嚴格的執行下去。 還有就是平時的一些技術積累以及知識存儲。知其然知其因此然,研究技術背後的底層原理,會對你有很大的幫助。好比說我要說來講說 ViewController 的生命週期,可能你們都會隨口說出 viewDidLoad
、viewWillAppear
等,我要問說說 View 的生命週期,可能就會有少數人茫然了。這些都是很基本的東西,可能你平時用不到,可是仍是須要你去了解他,注意細節。不少人可能會常常有這樣的困惑,好比我想寫一個圖片瀏覽器,可是我不知道該如何寫?寫完了性能如何?別人是怎麼寫的?這個就是須要平時的積累了,好比關於 UIText
相關的的你就得想到 YYText,數據存儲方面的你不只要知道老的 fmdb ,微信開源的 wcdb 有沒有去了解下呢?好比我就平時沒事喜歡在 GitHub 上看一些 star 比較高的開源庫,看看別人是怎麼實現的,想一想在個人項目中怎麼使用。舉個例子,最近阿里開源的 協程 框架 coobjc ,就在項目中使用,用來判斷用戶是否登陸
- (void)judgeLoginBlock:(void(^)(GCLoginStatus status))block {
co_launch(^{
NSDictionary *dic = await([self co_loginRequest]);
if (co_getError()) {
block(GCLoginStatusError);
} else if (dic) {
if ([dic[@"status"] intValue] == 1) {
block(GCLoginStatusLogin);
} else if ([dic[@"status"] intValue] == -99) {
block(GCLoginStatusUnLogin);
} else {
block(GCLoginStatusError);
}
} else {
block(GCLoginStatusError);
}
});
}
複製代碼
一眼看去邏輯就很簡單明瞭,比 Block 嵌套 Block 這種方式優雅的多。 如今只是重構的開始,如今已經完成的登陸的重構就 LoginViewController
而言,與以前相比就已經有很大的改變了(以前將近 800 行代碼,重構後只有 200 行),可能整體上各個模塊代碼加起來都差很少,可是爲 ViewController 減負後更加清晰明瞭了。後面重構完成後會出一個代碼量、包大小、性能等的對比,到時候再與你們分享!
Reference
三、iOS 如何實現Aspect Oriented Programming
五、BeeHive
七、coobjc