前言
本項(xiàng)目的數(shù)據(jù)為抓包所得薪棒,并且都是用的本地?cái)?shù)據(jù)手蝎,只作為學(xué)習(xí)用途。項(xiàng)目中所用到的appKey俐芯,為了方便調(diào)試棵介,不再刪除!但是僅作為本項(xiàng)目使用吧史!
寫這個(gè)項(xiàng)目之前也是對(duì)MVVM及RAC了解止于博客之類邮辽,寫之前花了幾天動(dòng)手寫了RAC的一些demo,然后才正式開始的項(xiàng)目,如果對(duì)RAC一點(diǎn)不了解的話逆巍,建議先看看RAC及FRP(函數(shù)響應(yīng)式編程)及塘,然后再看本項(xiàng)目。RACdemo锐极。
關(guān)于RAC及MVVM
- RAC-函數(shù)響應(yīng)式編程(FRP)的一個(gè)重量級(jí)的庫笙僚,學(xué)習(xí)難度較為陡峭,不過極大的簡(jiǎn)化代碼灵再,統(tǒng)一了消息傳遞機(jī)制肋层。另外就是性能較原生的有一定的差距,當(dāng)然翎迁,硬件的提升這些差距基本上會(huì)感覺不到栋猖。
- MVVM-不管是MVVM還是MVP、VIEPR或者M(jìn)V(X)汪榔,用意皆在使代碼結(jié)構(gòu)清晰蒲拉、易于維護(hù)、易于測(cè)試痴腌。另外不管是MVC還是MVVM雌团,都有兩種情況,1士聪、整個(gè)項(xiàng)目一個(gè)大的MVC锦援。2、每個(gè)模塊都有自己的MVC剥悟,比如首頁的MVC灵寺,我的頁面的MVC。各有有點(diǎn)吧区岗。
這兩點(diǎn)不再贅述略板,適合自己的、自己熟悉的才是最好用的躏尉, 另外蚯根,新的設(shè)計(jì)模式會(huì)使調(diào)試、debug的時(shí)間增加很多
pod
使用的第三方不多胀糜,除了RAC都是一般項(xiàng)目都有的
use_frameworks!
platform :ios, ‘8.0’
target “WTKWineMVVM” do
pod 'ReactiveCocoa', '4.2.2'
pod 'AFNetworking', '~> 3.1.0'
pod 'SVProgressHUD', '~> 2.0.3'
pod 'SDWebImage' , '3.7.3'
pod 'Masonry'
pod 'MJRefresh', '~> 3.1.12'
pod 'DZNEmptyDataSet', '~> 1.8.1'
pod 'Reachability', '~> 3.2'
pod 'MJExtension', '~> 3.0.13'
end
Common
wtk開頭的幾個(gè)是我開發(fā)中封裝的颅拦,這個(gè)建議開發(fā)中多思考,看那些是可以復(fù)用的(或者其他項(xiàng)目可以復(fù)用的)教藻,都盡量封裝起來距帅,方便以后使用。
- WTKQRCode 二維碼掃描的括堤,使用的系統(tǒng)的API碌秸,已經(jīng)封裝好绍移,QRCode連接。
- WTKStar 星級(jí)評(píng)價(jià)的view讥电,可以支持觸摸修改蹂窖、整形浮點(diǎn)型兩種,WTKStar連接
- WTKDropView 帶動(dòng)畫下拉列表恩敌,項(xiàng)目中有兩處用到瞬测,一個(gè)是還第一次寫的,沒有封裝好纠炮,第二次時(shí)封裝了一下月趟,所以還是建議多封裝,避免重復(fù)寫一樣的代碼恢口。WTKDropView連接
- WTKTransition 轉(zhuǎn)場(chǎng)動(dòng)畫孝宗,項(xiàng)目中的push、pop動(dòng)畫都是圓形擴(kuò)散的耕肩,項(xiàng)目中用的也是還沒有封裝好的因妇,需要借助basedViewController來實(shí)現(xiàn),后來封裝了一個(gè)猿诸,兩行代碼可以實(shí)現(xiàn)沙峻。使用中,如果某界面有手勢(shì)與pop手勢(shì)沖突两芳,把pop手勢(shì)從view上刪除即可。WTKTransition連接
Based
這里面包括了tabbarController去枷、navigationController怖辆、basedViewController、basedViewModel删顶、viewModelServices竖螃、viewModelNavigationImpl
tabbarController
tabbarController主要有添加子控制器、廣告頁逗余、監(jiān)聽badgeValue特咆、讀取本地?cái)?shù)據(jù)、自定義切換動(dòng)畫录粱,
-
切換動(dòng)畫
切換動(dòng)畫.gif
- (void)beginAnimation
{
CATransition *animation = [[CATransition alloc]init];
animation.duration = 0.5;
animation.type = kCATransitionFade;
animation.subtype = kCATransitionFromRight;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation.accessibilityFrame = CGRectMake(0, 64, kWidth, kHeight);
[self.view.layer addAnimation:animation forKey:@"switchView"];
}
- 監(jiān)聽bageValue
實(shí)際上就是監(jiān)聽購物車的總數(shù)(單例類的一個(gè)屬性)腻格,然后設(shè)置下標(biāo),這里使用RACObserver代替KVO實(shí)現(xiàn)啥繁。
@weakify(self);
[RACObserve([WTKUser currentUser], bageValue) subscribeNext:^(id x) {
@strongify(self);
UIViewController *vc = self.viewControllers[3];
NSInteger num = [x integerValue];
dispatch_async(dispatch_get_main_queue(), ^{
if (num > 0)
{
[vc.tabBarItem setBadgeValue:[NSString stringWithFormat:@"%ld",num]];
}
else
{
[vc.tabBarItem setBadgeValue:nil];
}
});
}];
navigationController
一般項(xiàng)目中菜职,只有一級(jí)界面顯示tabbar,所以有許多地方push的時(shí)候都會(huì)隱藏旗闽,所以在navigation中酬核,可以實(shí)現(xiàn)push方法蜜另,然后隱藏,其他地方都不用再處理
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (self.viewControllers.count > 0)
{
viewController.hidesBottomBarWhenPushed = YES;
}
[super pushViewController:viewController animated:animated];
}
另外navigation還有轉(zhuǎn)場(chǎng)動(dòng)畫相關(guān)的代理嫡意,不再多說举瑰。
BasedViewController
basedVC主要是配置一些通用的東西,比如屬性viewModel蔬螟、背景色此迅、返回按鈕以及MVVM的核心Bind(綁定)方法。使用basedVC的好處就是一處配置促煮,整個(gè)項(xiàng)目通用邮屁。
if (self.navigationController && self != self.navigationController.viewControllers.firstObject)
{
[self resetNaviWithTitle:@""];
UIPanGestureRecognizer *popRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePopRecognizer:)];
[self.view addGestureRecognizer:popRecognizer];
popRecognizer.delegate = self;
}
如果不是一級(jí)頁面毯焕,則會(huì)自動(dòng)添加返回按鈕韩玩。
bindViewModel
- (void)bindViewModel
{
RAC(self.navigationItem,title) = RACObserve(self.viewModel, title);
}
這里只是完成了title的綁定,因?yàn)槊看蝡ush的都是viewModel而不是viewController鳖擒,所以viewModel也聲明了一個(gè)title的屬性绳匀。
basedViewModel
主要是實(shí)現(xiàn)了構(gòu)建方法芋忿、登錄相關(guān)。
- (instancetype)initWithService:(id<WTKViewModelServices>)service params:(NSDictionary *)params
{
self = [super init];
if (self)
{
self.title = params[@"title"];
self.params = params;
self.services = service;
}
return self;
}
每次創(chuàng)建需要傳一個(gè)service和param疾棵,service用來push戈钢,不過這個(gè)項(xiàng)目一開始并沒有用這個(gè),所以比較遺憾是尔。param用來傳值殉了,title必須有!拟枚!薪铜。
WTKViewModelServices協(xié)議
協(xié)議,協(xié)議方法為push恩溅、pop等隔箍,
- (void)pushViewModel:(WTKBasedViewModel *)viewModel animated:(BOOL)animated;
- (void)popViewControllerWithAnimation:(BOOL)animated;
- (void)popToRootViewModelWithAnimation:(BOOL)animated;
- (void)presentViewModel:(WTKBasedViewModel *)viewModel animated:(BOOL)animated complete:(void(^)())complete;
///模態(tài)彈出vc,用于alert
- (void)presentViewController:(UIViewController *)viewController animated:(BOOL)animated complete:(void(^)())complete;
由于一開始并沒有想的太多脚乡,所以一開始并沒有寫模態(tài)蜒滩,以至于需要彈出alert的時(shí)候,需要把vc傳給viewModel奶稠。后來才加上的這個(gè)協(xié)議俯艰,所以一個(gè)好的架構(gòu)師相當(dāng)?shù)闹匾?/p>
WTKViewModelNavigationImpl
實(shí)現(xiàn)了WTKViewModelServices
協(xié)議,也就是push窒典、pop都會(huì)走這里蟆炊。由于最后還要pushViewController,而viewModel里面也沒有包含vc瀑志,所以push的時(shí)候涩搓,還要指定vc的name污秆,也是一個(gè)缺陷吧。
*push方法
WTKRecommendViewModel *viewModel = [[WTKRecommendViewModel alloc]initWithService:self.services params:@{@"title":@"推薦有獎(jiǎng)"}];
self.naviImpl.className = @"WTKRecommendVC";
[self.naviImpl pushViewModel:viewModel animated:YES];
Tools
有按功能創(chuàng)建的工具類昧甘,還有各種公用的tool良拼,
- dataManager 主要是用戶數(shù)據(jù)相關(guān)的一些方法,保存充边、讀取庸推、刪除。
- shoppingManager 存儲(chǔ)購物車數(shù)據(jù)浇冰。
- requestManager 網(wǎng)絡(luò)請(qǐng)求類贬媒。 使用了RAC,一般網(wǎng)絡(luò)請(qǐng)求的block也使用了RACSignal代替肘习,方法中傳一個(gè)signal际乘,或者返回一個(gè)signal。這里選擇了返回一直signal漂佩。
+ (RACSignal *)postDicDataWithURL:(NSString *)urlString
withpramater:(NSDictionary *)paremater
{
CGFloat time = arc4random()%15 / 10.0;
NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:urlString ofType:nil]];
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:dic];
[subscriber sendCompleted];
return nil;
}] delay:time];
}
由于是加載的本地?cái)?shù)據(jù)脖含,所以模擬了網(wǎng)絡(luò)延遲。
- WTKTool 項(xiàng)目中一些常用的方法(分享投蝉、登錄养葵、購物車動(dòng)畫、指紋驗(yàn)證等等)
如果是用AFN請(qǐng)求數(shù)據(jù)瘩缆,則用下面的方法
+ (RACSignal *)getWithURL:(NSString *)urlString withParamater:(NSDictionary *)paramter
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer.timeoutInterval = 5;
RACSubject *sub =[ RACSubject subject];
[manager GET:urlString parameters:paramter progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[sub sendNext:@{@"code":@100,@"data":responseObject}];
[sub sendCompleted];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[sub sendNext:@{@"code":@-400,@"data":@"請(qǐng)求失敗"}];
[sub sendCompleted];
}];
return sub;
}
RACSubject為RACSignal的子類关拒,可以允許先創(chuàng)建,再發(fā)送信號(hào)庸娱,所以使用RACSubject夏醉。
- mapManager 地圖相關(guān)。
實(shí)現(xiàn)
- 因?yàn)槎嘤媒壎ㄓ亢⑶液瘮?shù)響應(yīng)式編程,只需要關(guān)心結(jié)果氯夷,所以項(xiàng)目中基本所有的屬性基本都用懶加載臣樱,避免綁定時(shí)還沒有創(chuàng)建
下面以幾個(gè)頁面來說一下MVVM具體使用。
-
homeVC
viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cancelPop) name:@"wtk_cancelPop" object:nil];
self.automaticallyAdjustsScrollViewInsets = NO;
[self bindViewModel];
[self configView];
}
非常簡(jiǎn)短腮考,監(jiān)聽取消側(cè)滑返回雇毫,綁定viewModel,初始化view踩蔚。
下面主要說說bindViewModel
跟viewDidLoad一樣棚放,需要在bindViewModel中實(shí)現(xiàn)[super bindViewModel]
綁定數(shù)據(jù)
@weakify(self);
// 綁定數(shù)據(jù)
RAC(self.collectionView,headArray) = RACObserve(self.viewModel, headData);
RAC(self.collectionView,dataArray) = RACObserve(self.viewModel,dataArray);
self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
@strongify(self);
[self.viewModel.refreshCommand execute:self.collectionView];
}];
[self.collectionView.mj_header beginRefreshing];
// navi
RAC(self,leftButton.rac_command) = RACObserve(self.viewModel, naviCommand);
解釋一下,RAC(...)把某個(gè)對(duì)象的屬性與信號(hào)綁定起來馅闽。這里把collectionView的dataArray與viewModel的dataArray綁定飘蚯。
collectionView的刷新方法馍迄,讓viewModel的refreshCommand執(zhí)行,并且把collectionView傳遞過去局骤。
另外攀圈,RAC把許多類都添加的屬性,一般都是control有關(guān)的峦甩。比如最后一行的leftbtn的rac_command赘来。
-
homeViewModel
實(shí)現(xiàn)了業(yè)務(wù)相關(guān)的邏輯、網(wǎng)絡(luò)請(qǐng)求凯傲。 .h文件如下
/**刷新數(shù)據(jù)*/
@property(nonatomic,strong)RACCommand *refreshCommand;
@property(nonatomic,strong)NSArray *headData;
@property(nonatomic,strong)NSArray *dataArray;
///頭視圖
@property(nonatomic,strong)RACCommand *headCommand;
///中間按鈕點(diǎn)擊
@property(nonatomic,strong)RACCommand *btnCommand;
///good
@property(nonatomic,strong)RACCommand *goodCommand;
///導(dǎo)航欄
@property(nonatomic,strong)RACCommand *naviCommand;
@property(nonatomic,strong)RACSubject *searchSubject;
collectionView不需要再實(shí)現(xiàn)傳遞事件的block犬辰,只需要把viewModel傳給collectionView,點(diǎn)擊方法中執(zhí)行響應(yīng)的command即可冰单。
-
categoryVC(分類)
- (void)bindViewModel
{
[super bindViewModel];
@weakify(self);
[self.viewModel.refreshCommand execute:@[self.leftTableView,self.rightTableView]];
// 綁定數(shù)據(jù)
RAC(self,leftDataArray) = RACObserve(self.viewModel, leftArray);
RAC(_rightTableView,sectionArray) = RACObserve(self.viewModel, leftArray);
RAC(_rightTableView,dataDic) = RACObserve(self.viewModel, dataDic);
RAC(self.siftView,dataArray) = RACObserve(self.viewModel, selectArray);
self.rightTableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
@strongify(self);
[self.viewModel.refreshCommand execute:@[self.leftTableView,self.rightTableView]];
}];
// 右側(cè)tableView滑動(dòng)
[self.viewModel.rightCommand.executionSignals.switchToLatest subscribeNext:^(id x) {
@strongify(self);
NSIndexPath *indexPath = x;
[self.leftTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.section inSection:0] animated:YES scrollPosition:UITableViewScrollPositionTop];
}];
// 需要傳值幌缝,所以不這樣寫
// RAC(self.rightBtn,rac_command) = RACObserve(self.viewModel, selectedCommand);
// 點(diǎn)擊篩選按鈕
[[self.rightBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
[self resetSiftView];
if (self.isFirstSift)
{
[self.viewModel.selectedCommand execute:@[self.leftTableView,self.rightTableView,self.siftView]];
self.isFirstSift = NO;
}
}];
// 移除siftView
[self.siftView.dismissSubject subscribeNext:^(id x) {
// 消失
@strongify(self);
[self.viewModel beginDismissAnimation:@[self.leftTableView,self.rightTableView]];
}];
}
先刷新數(shù)據(jù),并且把left球凰、right tableView傳過去狮腿,供刷新使用。
綁定數(shù)據(jù)呕诉,綁定刷新方法缘厢,button使用rac的話,一種是直接綁定它的rac_command甩挫,另外一種就是上面代碼的那種贴硫,如果綁定rac_command,則傳過去的只是一個(gè)btn伊者,需要其他傳值的時(shí)候英遭,使用上面的方法。
-
cateViewModel
- requestManager的用法
RACSignal *signal = [WTKRequestManager postArrayDataWithURL:@"CategoryAllGoods" withpramater:@{}];
[signal subscribeNext:^(id x) {
// NSLog(@"%@",x);
[leftTableView reloadData];
[rightTableView reloadData];
[SVProgressHUD dismiss];
if([rightTableView.mj_header isRefreshing])
{
[rightTableView.mj_header endRefreshing];
}
}];
獲取網(wǎng)絡(luò)請(qǐng)求的signal,然后訂閱即可亦渗。
-
shoppingCarVC
購物車界面則主要是價(jià)格的監(jiān)聽挖诸,刪除、選中物品的邏輯法精。只有本次啟動(dòng)app后添加到購物車的商品才會(huì)默認(rèn)選中多律,讀取的本地購物車數(shù)據(jù),默認(rèn)沒有選中搂蜓。
為了簡(jiǎn)便處理狼荞,給商品添加了一個(gè)w_isSelected屬性,表示是否選中帮碰。
全選按鈕:
[[self.selectAllBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
self.isClickAllBtn = YES;
self.viewModel.isClickAllBtn = YES;
UIButton *btn = x;
btn.selected = !btn.selected;
SHOPPING_MANAGER.flag = NO;
NSArray *array = [SHOPPING_MANAGER.goodsDic allValues];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
WTKGood *good = obj;
good.w_isSelected = btn.selected;
if (idx == array.count - 1)
{
[self.tableView w_reloadData];
SHOPPING_MANAGER.goodsDic;
}
}];
}];
RAC(self.selectAllBtn,selected) = RACObserve(self.viewModel, btnState);
isClickAllBtn相味,標(biāo)志當(dāng)前是否為點(diǎn)擊按鈕。
-
shoppingCarViewModel
主要說一下監(jiān)聽價(jià)格
// - 監(jiān)聽價(jià)格
[RACObserve([WTKShoppingManager manager], changed) subscribeNext:^(id x) {
static BOOL isFirst;//是否是第一次檢測(cè)到?jīng)]有選中殉挽。用來避免多次改變selectAllBtn
isFirst = YES;
SHOPPING_MANAGER.flag = YES;
NSDictionary *dic = SHOPPING_MANAGER.goodsDic;
[[dic allValues] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
@strongify(self);
WTKGood *good = obj;
if(isFirst && !good.w_isSelected && !self.isClickAllBtn)
{
// self.selectAllBtn.selected = !self.selectAllBtn;
isFirst = NO;
self.btnState = NO;
}
if (idx == 0)
{
SHOPPING_MANAGER.price = 0;
}
if (good.w_isSelected)
{
SHOPPING_MANAGER.price += good.price * good.num;
}
if (idx == [dic allValues].count - 1 && isFirst && !self.isClickAllBtn)
{
self.btnState = YES;
}
if(idx == [dic allValues].count - 1)
{
// self.isClickAllBtn = NO;
self.isClickAllBtn = NO;
}
// self.priceLabel.text = [NSString stringWithFormat:@"共¥ %.2f",SHOPPING_MANAGER.price];
self.price = [NSString stringWithFormat:@"共¥ %.2f",SHOPPING_MANAGER.price];
}];
SHOPPING_MANAGER.flag = NO;
[self.emptySubject sendNext:@([dic allValues].count)];
}];
由于不能監(jiān)聽數(shù)組丰涉、字典等容器類屬性拓巧,所以在shoppingManager中,聲明了一個(gè)change的屬性昔搂,監(jiān)聽這個(gè)屬性來獲取實(shí)時(shí)的購物車數(shù)據(jù)玲销。每次添加、刪除購物車數(shù)據(jù)摘符,都會(huì)改變change這個(gè)屬性贤斜,來傳遞數(shù)據(jù)。flag屬性來判斷當(dāng)前是操作購物車數(shù)據(jù)還是監(jiān)聽逛裤,監(jiān)聽的話就不再改變change瘩绒,以避免死循環(huán)。
-
goodVC(商品詳情)
商品詳情為h5頁面带族,不再多說锁荔。評(píng)論的cell,帶圖的和不帶圖的使用的是同一個(gè)cell蝙砌,合理的利用cell阳堕,會(huì)減少不必要的冗余。
-
loginVC
這個(gè)項(xiàng)目除了cell只有這一個(gè)頁面使用的xib布局择克,登錄頁面使用MVVM更加典型恬总,所以詳細(xì)解釋一下這個(gè)頁面。
首先是viewDidLoad
- (void)viewDidLoad {
[super viewDidLoad];
[self bindViewModel];
[self initView];
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageFromColor:WTKCOLOR(255, 255, 255, 0.99)] forBarMetrics:UIBarMetricsDefault];
}
initView
主要是設(shè)置view的相關(guān)屬性肚邢,不多說壹堰。
bindViewModel
- (void)bindViewModel
{
[super bindViewModel];
@weakify(self);
RAC(self.viewModel,phoneNum) = self.phoneTextField.rac_textSignal;
RAC(self.viewModel,codeNum) = self.psdTextField.rac_textSignal;
RAC(self.loginBtn,enabled) = self.viewModel.canLoginSignal;
RAC(self.codeBtn,enabled) = self.viewModel.canCodeSignal;
[[self.codeBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
[self.viewModel.codeCommand execute:x];
}];
[[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
@strongify(self);
[self.viewModel.loginCommand execute:x];
}];
[self.viewModel.loginCommand.executionSignals.switchToLatest subscribeNext:^(id x) {
if ([x[@"code"] integerValue] == 100)
{
@strongify(self);
[self.navigationController popViewControllerAnimated:YES];
}
}];
}
前兩個(gè)個(gè)RAC(self.viewModel,phoneNum) = textField.rac_textSignal
把textField的text賦值給viewModel的phoneNum,并不是只賦值一次,每一次textField改變骡湖,都會(huì)重新給phoneNum賦值.
RAC(self.loginBtn,enable) = self.viewModel.canLoginSignal
把viewModel的canLoginSignal
賦值給loginBtn的enable屬性贱纠,控制loginBtn的狀態(tài).
下面兩個(gè)block為登錄和獲取驗(yàn)證碼按鈕的點(diǎn)擊方法,也可以寫成下面這樣的
RAC(self.codeBtn,rac_command) = RACObserve(self.viewModel, codeCommand)
也就是點(diǎn)擊按鈕响蕴,viewModel的command會(huì)執(zhí)行谆焊。
-
loginViewModel
代碼:
- (void)initViewModel
{
@weakify(self);
RACSignal *phoneSignal = [RACObserve(self, phoneNum) map:^id(id value) {
@strongify(self);
return @([self isPhoneNum:value]);
}];
RACSignal *codeSignal = [RACObserve(self, codeNum) map:^id(id value) {
@strongify(self);
return @([self isCodeNum:value]);
}];
self.canLoginSignal = [RACSignal combineLatest:@[phoneSignal,codeSignal]
reduce:^id(NSNumber *phone,NSNumber *code){
return @([phone boolValue] && [code boolValue]);
}];
self.canCodeSignal = [RACSignal combineLatest:@[phoneSignal]
reduce:^id(NSNumber *phone){
return @([phone boolValue]);
}];
self.codeCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
UIButton *btn = input;
btn.enabled = NO;
self.time = 60;
[btn setTitle:[NSString stringWithFormat:@"%ld",self.time] forState:UIControlStateNormal];
__block NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(updateCodeTime:) userInfo:btn repeats:YES];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(60 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[timer invalidate];
timer = nil;
btn.enabled = YES;
[btn setTitle:@"驗(yàn)證" forState:UIControlStateNormal];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(arc4random() % 12 / 15.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
SHOW_SUCCESS(@"發(fā)送成功");
DISMISS_SVP(1.2);
});
return [RACSignal empty];
}];
self.loginCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
[WTKTool login];
CURRENT_USER.phoneNum = self.phoneNum;
[WTKDataManager saveUserData];
SHOW_SUCCESS(@"登錄成功");
DISMISS_SVP(1);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@{@"code":@100}];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
NSLog(@"信號(hào)被銷毀");
}];
}];
}];
}
- (BOOL)isPhoneNum:(NSString *)phoneNum
{
if ([phoneNum hasPrefix:@"1"])
{
return phoneNum.length == 13;
}
return NO;
}
- (BOOL)isCodeNum:(NSString *)code
{
return [code integerValue] == self.code;
}
-
phoneSignal
- 監(jiān)聽phoneNum,并且判斷當(dāng)前的phoneNum是否為正確的手機(jī)號(hào)浦夷。 -
codeSignal
與phoneSignal相同懊渡,判斷當(dāng)前code是否為正確的驗(yàn)證碼(是否等于@“1234”)。 -
self.canLoginSignal
- 將phoneSignal與codeSignal合并成一個(gè)信號(hào)军拟,如果兩個(gè)同時(shí)為YES,則canLoginSignal會(huì)發(fā)一個(gè)內(nèi)容YES的信號(hào)誓禁。登錄頁面的loginBtn的enable會(huì)根據(jù)信號(hào)的內(nèi)容而改變懈息。 -
self.canCodeSignal
與canLoginSignal類似,不過不是兩個(gè)信號(hào)的合并摹恰。 -
self.codeCommand
辫继,codeBtn的點(diǎn)擊方法怒见,與MVVM無關(guān),不再多說. -
self.loginCommand
,登錄方法姑宽,處理一些登錄的邏輯遣耍。
下面兩個(gè)方法為判斷手機(jī)號(hào)及驗(yàn)證碼的相關(guān)邏輯。
總結(jié)
使用MVVM+RAC寫完這個(gè)項(xiàng)目炮车,感覺很不錯(cuò)舵变,很屌的框架,根本就停不下來瘦穆。就算不用MVVM纪隙,也建議使用一下RAC框架開發(fā)試試。簡(jiǎn)化扛或、統(tǒng)一绵咱,就是不大量使用RAC,也可以使用它代替原來的代理熙兔、block悲伶、通知,把回調(diào)住涉、代理之類的寫一個(gè)函數(shù)里麸锉,使得一個(gè)業(yè)務(wù)的代碼寫在一個(gè)地方,比如項(xiàng)目中我的
頁面的導(dǎo)航欄漸變秆吵。并且使用通知及KVO淮椰,不用在dealloc中移除了,RAC已經(jīng)處理纳寂。
不過由于RAC由cocoa的OOP變成了FRP主穗,使得學(xué)習(xí)曲線陡峭,所以并沒有被大規(guī)模的采納毙芜,并且剛?cè)胧謺r(shí)忽媒,debug時(shí)間也會(huì)增加。
如果對(duì)你有幫助腋粥,可以在git上給個(gè)star晦雨,會(huì)持續(xù)更新。
項(xiàng)目連接
12.27更新
關(guān)于百度地圖報(bào)錯(cuò)隘冲,很多童鞋解決不了闹瞧,這里貼出來解決辦法。把報(bào)紅的刪除展辞,然后重新來進(jìn)來即可奥邮。文件位置(項(xiàng)目-vendor-baiduMap-baiduMapAPI_Map.framework-Resources-mapapi.bundle)
2020-04-07
百度網(wǎng)盤鏈接
提取密碼020h