iOS MVVM+RAC實(shí)戰(zhàn)詳解(高仿某電商項(xiàng)目)

項(xiàng)目連接

前言

本項(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

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

Paste_Image.png

有按功能創(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ì)減少不必要的冗余。

評(píng)論
  • loginVC

這個(gè)項(xiàng)目除了cell只有這一個(gè)頁面使用的xib布局择克,登錄頁面使用MVVM更加典型恬总,所以詳細(xì)解釋一下這個(gè)頁面。

login.gif

首先是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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子洽腺,更是在濱河造成了極大的恐慌脚粟,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蘸朋,死亡現(xiàn)場(chǎng)離奇詭異核无,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)藕坯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門团南,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人堕担,你說我怎么就攤上這事已慢。” “怎么了霹购?”我有些...
    開封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵佑惠,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我齐疙,道長(zhǎng)膜楷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任贞奋,我火速辦了婚禮赌厅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘轿塔。我一直安慰自己特愿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開白布勾缭。 她就那樣靜靜地躺著揍障,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俩由。 梳的紋絲不亂的頭發(fā)上毒嫡,一...
    開封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音幻梯,去河邊找鬼兜畸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛碘梢,可吹牛的內(nèi)容都是我干的咬摇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼煞躬,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼肛鹏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤龄坪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后复唤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體健田,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年佛纫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了妓局。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呈宇,死狀恐怖好爬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情甥啄,我是刑警寧澤存炮,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站蜈漓,受9級(jí)特大地震影響穆桂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜融虽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一享完、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧有额,春花似錦般又、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至句狼,卻和暖如春笋熬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腻菇。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來泰國打工胳螟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人筹吐。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓糖耸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親丘薛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嘉竟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容