MVVM我的实践(一)

这是随笔,写到哪算哪

【ViewModel的角色是什么】

ViewModel主要是为展示层(ViewController/View)提供数据和交互的服务。它可以让展示层拿到直接可以展示的数据。它可以代理展示层处理用户交互事件。当APP的流程围绕Controller推进时,ViewModel通常是依附于Controller作为它的一种功能代理和数据缓存角色的,这个时候我喜欢说ViewModel服务于Controller,但很多时候,我写的ViewModel是可以支配Controller的,这时候的ViewModel的作用就不是服务于Controller,而是它所代表的功能或流程的核心,是Controller服务于它。

ViewModel角色.png

===================直租直售模块我是业主列表Cell的ViewModel示例===================

下面是直租直售模块的我是业主列表。这个列表的好些元素有比较多的样式变化,和动态逻辑。比如底部状态,右上角小旗子,右下角按钮等都是根据不同数据会有不同外观、行为、数据的。这导致了这个列表的Cell如果按照常规方法去写,将不仅包含对这些子View的初始化,设值,还有一堆数据逻辑判断代码,以及为了处理这些子View的点击事件而写的事件处理代码。因为考虑到最容易膨胀、变动,最需要测试的部分的代码就是处理逻辑和交互事件的代码,所以我决定把这些代码放进ViewModel,以便能够让Cell更好地应付业务逻辑的变动,和让我能更方便、容易地分析这些逻辑代码。

A2我是业主.jpg

ViewModel 的第一项职能:为展示层提供数据。

下面的代码展示了Cell里面的View直接从Cell的ViewModel拿到了直接可用的数据,Cell本身不需要做判断、数据转换等工作,它只负责初始化sub views,并给它们设值。

- (void)updateWithViewModel:(OwnerHouseSourceCellViewModel *)viewModel{
    if (!viewModel || !self.contentView) {
        return;
    }
    //viewModel
    self.viewModel = viewModel;
    //cell是否可点击
    self.selectionStyle = viewModel.cellSelectionStyle;
    //楼盘名称
    self.projectNameLb.text = viewModel.projectName;
    //查看
    self.checkActionView.hidden = !self.viewModel.showCheckBtn;
    //房屋规格描述
    self.houseDescLb.text = viewModel.houseInfo;
    //出租类型
    self.rentTypeImgV.image = [UIImage imageNamed:viewModel.rentTypeIcon];
    @weakify(self);
    if (self.rentTypeImgV.image == nil) {
        [self.rentTypeImgV mas_updateConstraints:^(MASConstraintMaker *make) {
            @strongify(self);
            if (self.rentTypeImgV.image == nil) {
                make.right.mas_equalTo(self.housePriceLb.mas_left);
            }else{
                make.right.mas_equalTo(self.housePriceLb.mas_left).offset(-5);
            }
        }];
    }
    self.rentTypeTextView.textLines = @[viewModel.rentTypeText];
    //价格
    self.housePriceLb.text =  viewModel.price;
    //调价
    self.changePriceView.userInteractionEnabled = viewModel.adjustPriceEnable;
    //剩余展示天数
    self.showDaysImgV.image = [UIImage imageNamed:viewModel.leftShowDaysFlag];
    self.showDaysTextView.textLines = @[@"可展示",viewModel.leftShowDays];
    //阅览数
    self.viewedNumImgV.image = [UIImage imageNamed:viewModel.viewedCountFlag];
    self.viewedNumTextView.textLines = @[@"已浏览",viewModel.viewedCount];
    //删除按钮
    self.deleteActionView.hidden = !viewModel.showDeleteBtn;
    // 出租、出售按钮
    self.sellActionView.hidden = !viewModel.showSoldOrRentOutBtn;
    if (!self.sellActionView.hidden) {
        self.sellActionView.text = viewModel.soldOrRentOutBtnTitle;
    }
    self.unSellActionView.hidden = !viewModel.showNotSellOrRentBtn;
    if (!self.unSellActionView.hidden) {
        self.unSellActionView.text = viewModel.notSellOrRentBtnTitle;
    }
}

这个方法里面关于出租出售按钮部分,其实不用写这样的判断逻辑,因为这不应该是Cell的职责

// 出租、出售按钮 
self.sellActionView.hidden = !viewModel.showSoldOrRentOutBtn; 
if (!self.sellActionView.hidden) { 
    self.sellActionView.text = viewModel.soldOrRentOutBtnTitle;
}

只需要让cell把sellActionView的可能会变动的两个属性值直接跟ViewModel的相关属性值关联就行了,像下面这样:

// 出租、出售按钮 
self.sellActionView.hidden = !viewModel.showSoldOrRentOutBtn; 
self.sellActionView.text = viewModel.soldOrRentOutBtnTitle;

这样,Cell的这部分工作就只是,初始化sellActionView,以及在update数据方法里面,把sellActionView的相关暴露属性跟Cell的ViewModel的响应暴露属性关联起来,其他的事情不用Cell去管。
Cell变得简单了,轻量级了,那ViewModel势必承担了响应的逻辑判断,数据转换这样的职责。下面是OwnerHouseSourceCellViewModel.h代码:

@interface OwnerHouseSourceCellViewModel : NSObject

@property (nonatomic, weak) OwnerHouseSourceManageAppService *appService;
@property (nonatomic, assign) UITableViewCellSelectionStyle cellSelectionStyle;
@property (nonatomic, copy) NSString *projectName;
@property (nonatomic, assign) BOOL showCheckBtn;
@property (nonatomic, copy) NSString *houseInfo;
@property (nonatomic, copy) NSString *rentTypeIcon;
@property (nonatomic, copy) NSString *rentTypeText;
@property (nonatomic, copy) NSString *price;
@property (nonatomic, copy) NSString *leftShowDaysFlag;
@property (nonatomic, copy) NSString *leftShowDays;
@property (nonatomic, copy) NSString *viewedCountFlag;
@property (nonatomic, copy) NSString *viewedCount;
@property (nonatomic, assign) BOOL adjustPriceEnable;
@property (nonatomic, assign) BOOL showDeleteBtn;
@property (nonatomic, assign) BOOL showSoldOrRentOutBtn;
@property (nonatomic, copy) NSString *soldOrRentOutBtnTitle;
@property (nonatomic, assign) BOOL showNotSellOrRentBtn;
@property (nonatomic, copy) NSString *notSellOrRentBtnTitle;
@property (nonatomic, assign) BOOL showStatusBar;
@property (nonatomic, assign) BOOL statusBarInteractive;
@property (nonatomic, copy) NSString *arrowImage;
@property (nonatomic, copy) NSString *statusBarGrayText;
@property (nonatomic, copy) NSString *statusBarRedText;

- (instancetype)initWithModel:(HouseSourceManageModel *)model;
- (void)tapCheckBtn;
- (void)tapAdjustPriceView;
- (void)tapDdeleteBtn;
- (void)tapStatusBar;
- (void)tapSoldOrRentOutBtn;
- (void)tapNotSellOrRentBtn;

@end

它包含了有可能被Cell使用到的所有设置UI方面的属性,最方便的就是做到一个属性,控制一个View的外观,比如通过projectName控制楼盘标题的显示;

@property (nonatomic, copy) NSString *projectName;

或者通过一个属性可以控制一个View的行为,比如通过cellSelectionStyle控制Cell是否可以点击。

@property (nonatomic, assign) UITableViewCellSelectionStyle cellSelectionStyle;

更多的时候,要由多个属性来控制一个View的外观、状态和行为,比如要用showSoldOrRentOutBtn、showSoldOrRentOutBtn这两个属性控制一个按钮的显示与隐藏,显示什么标题。

@property (nonatomic, assign) BOOL showSoldOrRentOutBtn;
@property (nonatomic, copy) NSString *soldOrRentOutBtnTitle;

用多个属性控制一个View,是把跟这个View相关的逻辑操作放到ViewModel来的一种好方法,这样View就可以不用在Cell里面写逻辑判断了,只需让它绑定多个属性就行了。我们不用在Cell里面写有逻辑判断的代码:

if (hiddenStatus1){
    // show view
    if (contentStatus1){
        // show content1
    }else{
       //  show content2
    }
}else{
    // hide view
}

而换成写不用判断逻辑的代码:

view.hidden = viewModel.viewHidden;
view.content = viewModel.viewContent;

相关的判断和逻辑代码写在Cell的ViewModel里面:

- (BOOL)showView {
    if (hiddenStatus1) {
        return YES;
    } else {
        return NO;
    }
}

- (NSString *)shownContent {
    if (contentStatus1) {
        return content1;
    } else 
        return content2;
    }
}

将View的变更逻辑写在ViewModel里面有如下好处。一,如果以后UI显示逻辑有变动,而UI样式没变化,那么只需要改ViewModel里面的逻辑就行,不用动到Cell的代码;二,如果发现UI显示逻辑有误,我们只需要把精力放在分析ViewModel里面的逻辑就可以了,可以不用看Cell的代码

ViewModel 的第二项职能:响应交互事件。

我们很容易分析得出一个模块到底有哪些交互事件,那么这些交互事件大部分甚至全部都是应该放到ViewModel去处理的,除了那些不会改变数据和状态,又不需要跟其他模块交互的简单的纯UI操作(这样的操作通常应该比较少),要知道ViewModel代表一个模块的功能,那么它理应能够支持所有跟功能相关的操作。还是以直租直售的我是业主列表的Cell作为例子,它的ViewModel代理了所有跟Cell有关的操作。由于ViewModel本身掌管着数据,所以它的交互方法几乎可以不用传任何参数,在设计方法名方面,我是不建议使用- (void)xxxWithType:(NSInteger)type;这样的方法的,这样的方法让使用者费劲去理解type的用法,而且即使理解了,有一天方法内部type的意义变了,但是调用方又忘记了同步变更调用时的type,那么就会出错。所以我一般设计方法时,都要尽量保证一个方法只做一件事情,而且从方法名就要说明它做了什么事情。在这个Cell里面,UI的设计上,“已出售”“已出租”两种状态的UI用的是同一个view,那么点击这个view的时候,按理说我要么这样设计点击方法:
方式(一):

// type = 0,代表已出售;type = 1,代表已出租
- (void)tapSoldOrRentOutBtnWithType:(NSInteger)type;

但是如上所说,这是我所摒弃的设计,所以我通常应该选择下面的方式:
方式(二):

- (void)tapSoldOutBtn;
- (void)tapRentOutBtn;

但在这个ViewModel里面我为什么只用了一个方法来代表这个View的两种点击情况?
方式(三):

- (void)tapSoldOrRentOutBtn;

因为,根据情况选择执行不同的响应方法,这个ViewModel的职责而不是Cell的,所以,应该让Cell在调用事件响应处理方法时越简单越好。方式(三)的设计,让Cell在“已出售”/“已出租”这个View被点击时只需要调用- (void)tapSoldOrRentOutBtn;通知ViewModel就行了,不用做其他判断逻辑。由ViewModel内部来做后续处理。ViewModel内部这一方法的实现:

- (void)tapSoldOrRentOutBtn {
    if (根据内部数据分析,当前按钮显示的是已出售) {//内部数据得出的状态
        [self tapSoldOutBtn];
    } else {
        [self tapRentOutBtn];
    }
}

ViewModel 的第三项职能:通知展示层数据有更新。

ViewModel知道如何去获取数据,当获取到新数据后,它有几种方式来协助实现展示层的UI更新。通常我比较少用通知,因为大部分情况只是两个对象之间传输消息,实在不应该用通知;我也比较少用KVO,因为不想每次都要在Controller/View里面写监听代码。所以要么用协议,要么用block。如果我不想做分层,或者不想做基于协议的实现时,我通常也比较少用协议,因为跟使用KVO一样,我也不喜欢在使用ViewModel的地方都要把协议代码撒一遍,代码分散,不好管理。所以block成了大部分情况下我的自然选择,虽然它有也有些缺点,但我认为那些不重要。

**=========================小群组主页ViewModel示例============================== **

小群组主页模块的ViewModel主要的工作之一就是处理滑动菜单的未读消息badge的显示逻辑。效果图:


小群群活动.jpg

小群组主页ViewModel头文件:

#import "GroupMainAppService.h"

@interface GroupMainViewModel : NSObject

/// 大群Id
@property (nonatomic, assign) NSInteger teamId;
/// 云信sessionId
@property (nonatomic, copy) NSString *sessionId;
/// 群聊 群ID
@property (nonatomic, assign) NSInteger groupId;
/// 群聊 群名称
@property (nonatomic, copy) NSString *groupName;
/// 群聊 群主Id
@property (nonatomic, assign) NSInteger adminUserId;
/// 群聊 群状态
@property (nonatomic, assign) GroupStatus groupStatus;
/// 群聊 未读消息数 (由群消息列表进入时传)
@property (nonatomic, assign) NSInteger chatUnreadCount;
/// 当前激活模块是哪个
@property (nonatomic, assign) NSInteger currentPageIndex;

typedef void(^update)();

- (instancetype)initWithAppService:(id<GroupMainAppService>)service;
//获取数据
- (void)loadData;
//聊天窗口有新消息时调用
- (void)showChatUnreadCount:(NSInteger)unReadCount;
//聊天窗口有大于99条未读消息
- (void)hasLargeNumNewMessage:(update)update;
//聊天窗口有小于等于99条未读消息
- (void)hasNormalNumNewMessage:(void(^)(NSUInteger num))block;
//在消息免打扰的情况下有新的未读消息
- (void)hasNewMessage:(update)update;
//有新的群动态
- (void)hasNewDynamic:(update)update;
//有新的群活动
- (void)hasNewActivity:(update)update;
//群动态状态为已读
- (void)dynamicRead:(update)update;
//群活动状态为已读
- (void)activityRead:(update)update;
//消息已读
- (void)messageRead:(update)update;

@end

滑动菜单self.slideMenu是这个模块的Controller管理的一个子View,具有针对各个菜单项,显示多种风格badge的功能。在这里,我的设计是让它只负责执行badge的显示,至于什么时候,针对哪个菜单项,显示什么风格的badge,这些逻辑就放在ViewModel里面。下面是在Controller里面初始化ViewModel的代码:

- (GroupMainViewModel *)viewModel {
    if (!_viewModel) {
        _viewModel = [[GroupMainViewModel alloc] initWithAppService:self.appService];
        _viewModel.groupId = self.groupId;
        _viewModel.groupStatus = self.groupStatus;
        _viewModel.sessionId = self.sessionId;
        _viewModel.adminUserId = self.adminUserId;
        _viewModel.chatUnreadCount = self.chatUnreadCount;
        _viewModel.currentPageIndex = self.slideMenu.currentPageIndex;
        @weakify(self);
        [_viewModel hasLargeNumNewMessage:^() {
            @strongify(self);
            [self.slideMenu showMoreBadgeForItem:0];
        }];
        [_viewModel hasNormalNumNewMessage:^(NSUInteger num) {
            @strongify(self);
            [self.slideMenu showBadgeText:[NSString stringWithFormat:@"%@",@(num)] forItem:0];
        }];
        [_viewModel hasNewMessage:^{
            @strongify(self);
            [self.slideMenu showDotBadgeForItem:0];
        }];
        [_viewModel hasNewDynamic:^{
            @strongify(self);
            [self.slideMenu showDotBadgeForItem:1];
        }];
        [_viewModel hasNewActivity:^{
            @strongify(self);
            [self.slideMenu showDotBadgeForItem:2];
        }];
        [_viewModel messageRead:^{
            @strongify(self);
            [self.slideMenu showNoneBadgeForItem:0];
        }];
        [_viewModel dynamicRead:^{
            @strongify(self);
            [self.slideMenu showNoneBadgeForItem:1];
        }];
        [_viewModel activityRead:^{
            @strongify(self);
            [self.slideMenu showNoneBadgeForItem:2];
        }];
    }
    return _viewModel;
}

为了让self.slideMenu不用思考就懂得如何显示badge,那么ViewModel就得把工作做到足够细致。这里,ViewModel把各种要变更badge的情形都通过方法暴露出来了,这些方法都是具备异步通知UI做刷新的能力的,Controller只需要设置好针对每种情形要调用的UI刷新方法就好,何时触发这些刷新方法是ViewModel的事。这里ViewModel的方法的命名我让它们展示出的是业务意义,而非操作意义,比如关于在聊天子控制器菜单那里,未读消息badge的显示规则是免打扰情况下显示红点,少于100条显示数字,多于等于100条显示...。我不会把方法命名为:

//为聊天菜单项显示省略号风格的badge
- (void)showMoreStyleBadgeForChat:(update)update;
//为聊天菜单项显示数字风格的badge
- (void)showTextStyleBadgeForChat:(void(^)(NSUInteger num))block;
//为聊天菜单项显示红点风格的badge
- (void)showDotStyleBadgeForChat:(update)update;

因为这种风格的命名太具体,太细节,底层操作意味太浓,怎么操作UI刷新不是ViewModel该管的事,它只需要显式地告诉调用者,会有哪些业务状态的变化需要让你知道,会有哪些业务操作可以让你去执行。因为以后如果别的地方也使用这个ViewModel,但是它用到更新UI的方式变了,它不是改变菜单项的badge了,而是做了别的UI刷新操作,那么这些ViewModel暴露的方法名,应该能在那种新的应用情形下也具备相同的业务意义。所以像下面的命名,我觉得更好:

//Controller要处理这写业务状态的操作就在响应的update的block里面做。做什么ViewModel不管,viewModel的方法名也不会暗示Controller去做什么。
//只告诉使用方当前业务状态是有很多未读消息
- (void)hasLargeNumNewMessage:(update)update;
//只告诉使用方当前业务状态是有一些未读消息
- (void)hasNormalNumNewMessage:(void(^)(NSUInteger num))block;
//只告诉使用方当前业务状态是有未读消息,而聊天设置了免打扰
- (void)hasNewMessage:(update)update;

上面描述了ViewModel针对业务状态的改变,要如何设计出具备业务意义的方法名来通知使用方什么状态发生了更改,以便于使用方知道在哪里设置做相应的UI操作。下面讲的保持具备统一、通用业务意义的方法名的另一种情况,就是揭示调用方法后将会触发什么业务操作。例子是上面提到的我是业主列表的OwnerHouseSourceCellViewModel,写这个ViewModel时,考虑得也不够周到,所以在响应交互的方法名的设计上,遗留了很多底层、细节、操作性的痕迹,没有揭示业务意义,这些方法名如下:

//点击查看按钮
- (void)tapCheckBtn;
//点击调价按钮
- (void)tapAdjustPriceView;
//点击删除按钮
- (void)tapDdeleteBtn;
//点击状态栏
- (void)tapStatusBar;
//点击已出售或者已出租按钮
- (void)tapSoldOrRentOutBtn;
//点击不卖了或者不租了按钮
- (void)tapNotSellOrRentBtn;

如果按照上面的原则去优化,应该改为:

//查看房源详情
- (void)checkHouseDetails;
//调价房源价格
- (void)adjustHousePrice;
//删除房源
- (void)deleteHouse;
//重新编辑、重新发布房源
- (void)editRepublishHouse;
//将房源设置为已出租或已出售
- (void)soldOrRentOutHouse;

这样的话,ViewModel只需要看它暴露的方法就知道它的业务功能,某些表现层的使用方式不会对它下一次的使用造成认知干扰。

【不与Controller打交道,把流程交给ViewModel】

使用ViewModel后,我发觉不能只把ViewModel困于某个Controller内作为它的功能附属,而可以让ViewModel代替Controller去执行流程性的工作。这不知不觉解决了一些我以前觉得棘手的问题。比如控制器的关联问题,当我们要进入某个功能流程时,通常需要引用相关的控制器,然后push,有些流程,有某种枢纽的作用,所以它的控制器关联了好多其他的控制器。这种关联导致当一个控制器要改变的时候,比如丢弃了,换名字了,换了交接方式等,那么关联它的控制器代码也要跟着变动。以前把很多重要代码写在控制器的方式,以控制器驱动程序的运行,让控制器变得很重要,它的频繁改动是我们不期望的。有了ViewModel后,我让ViewModel有了更多的作用,不仅是我们push进入一个Controller后把所有工作交给它,而且做到甚至不用知道Controller的存在,我们也能通过ViewModel来执行下一个流程,也就是让ViewModel决定下一步该push到那个Controller。

===================找房需求的ViewModel示例===================
有一个这样的需求:每次APP重新启动后,默认进入头条板块,如果检测到上一次退出之前有未完成的找房需求,那么就跳入到找房需求相关页面。我在要决定是否发生跳转的控制器导入了初始化ViewModel所需的相关文件,并初始化ViewModel:

#import "FindHouseDemandAppService.h"
#import "FindHouseDemandViewModel.h"

@interface ArticleListContainerViewController () 

@property (nonatomic, strong) FindHouseDemandViewModel *findHouseViewModel;
@property (nonatomic, strong) FindHouseDemandAppService *findHouseAppService;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.findHouseAppService = [[FindHouseDemandAppService alloc] initWithNavigationController:self.navigationController];
    self.findHouseViewModel = [[FindHouseDemandViewModel alloc] initWithAppService:self.findHouseAppService];
}

当要执行跳转的时候,通过对ViewModel做必要的属性赋值,并执行相关的业务操作的方法,来进入到下一步的业务流程。至于跳转到下一个Controller后,还是不是原来的Controller?还是不是按原来的流程跳转?这些都不重要了,这些都不用上一个流程的代码关心,因为影响不到它们。在任何时候,只要拿到FindHouseDemandViewModel,就能够让它做跟找房需求相关的功能,走相关的流程。

// 决定是否要跳转,要怎么跳转到找房需求模块
- (void)needHandleDemand {
    /// 省略前面的代码///
            if ([brokerArray count] > 0) {
                // 周边中介 -> 我要找中介 -> 选择经纪人
                [self.findHouseViewModel.brokerArray addObjectsFromArray:brokerArray];
                self.findHouseViewModel.demandId = [OAuthClient shareInstance].getMainExtendInfoModel.needHandleDemandId;
                [OAuthClient shareInstance].getMainExtendInfoModel.needHandleDemandId = 0;
                self.findHouseViewModel.processingDemandType = [OAuthClient shareInstance].getMainExtendInfoModel.needHandleDemandType;
                self.findHouseViewModel.fromType = FindHouseDemandFromAppRestart;
                self.findHouseViewModel.localAgencytimer = second;
                //将跳转到响应经纪人列表
                [self.findHouseViewModel showResponsedBrokers];
                
            } else {
                // 周边中介 -> 我要找中介 -> 即约即看
                self.findHouseViewModel.demandId = [OAuthClient shareInstance].getMainExtendInfoModel.needHandleDemandId;
                self.findHouseViewModel.processingDemandType = [OAuthClient shareInstance].getMainExtendInfoModel.needHandleDemandType;
                [OAuthClient shareInstance].getMainExtendInfoModel.needHandleDemandId = 0;
                self.findHouseViewModel.demandpushcount = brokerCount;
                self.findHouseViewModel.fromType = FindHouseDemandFromAppRestart;
                self.findHouseViewModel.timer = second;
                self.findHouseViewModel.latitude = userLat;
                self.findHouseViewModel.longitude = userLng;
                //将跳转到搜索经纪人地图
                [self.findHouseViewModel sendFindHouseDemand];
            }
            
    ///省略后面的代码///
}

为什么相比关联Controller我更喜欢关联ViewModel呢,因为我觉得ViewModel代表了一个App的血肉,一个个ViewModel连城一个整体就是App的生命脉络,只要这些ViewModel组成的网还在,App的灵魂就没有变,还是那个App。但是Controller/View呢?它们是App的外衣,容器,它们应该能够后轻易地被替换,所以当涉及到流程的交接时,我觉得交给ViewModel才能够保证稳定性,而只有当流程没有很紧密地与Controller关联时,Controller和依附在其上面的View才能被轻松地替换,这样才能让App轻易实现换装。

【不与Controller/View直接关联的ViewModel如何执行需要它们支持才能做的工作呢?】

只要把流程都交给ViewModel那么它就必须得去做一些原本由Controller和View来做的事情,否则它这个流程就不可能走得完整。但是ViewModel是不应该直接关联View的,而且它也不应该能直接操控Controller,否则它们的耦合性就难免变得纠缠。不过,不能直接做的事情,可以代理给别的类来做。在我用到ViewModel的地方基本都搭配了一个或多个AppService类,专门来协助ViewModel来做一些它不宜直接介入的事情,总之是各种杂活。


appservice0.png

appservice1.png

appservice2.png

AppService通常不是ViewModel拥有的,通常是在初始化ViewModel时作为参数被传入,然后被ViewModel内部的弱引用属性引用。

///.h文件
#import "FindHouseDemandAppService.h"

- (instancetype)initWithAppService:(FindHouseDemandAppService *)service;

@end

///.m文件
#import "FindHouseDemandViewModel.h"

@property (weak, nonatomic) FindHouseDemandAppService *service;

@end

@implementation FindHouseDemandViewModel

- (instancetype)initWithAppService:(FindHouseDemandAppService *)service {
    if (self = [super init]) {
        self.service = service;
    }
    return self;
}

当然也可以根据情况,把它作为ViewModel的一个属性,通过赋值给到ViewModel。AppService相当于给ViewModel提供了一个服务层,所有ViewModel无能为力的事情,都委托给这个服务层去做,ViewModel要做的就是调用服务层暴露的方法,提供数据,设置回调处理。比如OwnerHouseSourceCellViewModel就是通过OwnerHouseSourceManageAppService实现了它能为OwnerHouseSourceManageListCell所做的所有交互支持。

#import <Foundation/Foundation.h>

@interface OwnerHouseSourceManageAppService : NSObject

- (instancetype)initWithNavigation:(UINavigationController *)navVC;

//重新提交出售房源审核
- (void)resubmitForSaleHouseSource:(NSInteger)sourceId;
//重新提交出租房源审核
- (void)resubmitForRentHouseSource:(NSInteger)sourceId;
/*重新发布的行为其实就是重新提交审核*/
//发布出售房源
- (void)publishForSaleSource;
//发布出租房源
- (void)publishForRentSource;
//查看出售房源详情
- (void)checkForSaleSource:(NSInteger)sourceId url:(NSString *)url;
//查看出租房源详情
- (void)checkForRentSource:(NSInteger)sourceId url:(NSString *)url;
//出售房源调价
- (void)adjustPriceToForSaleSource:(NSInteger)sourceId originPrice:(NSString *)price;
//出租房源调价
- (void)adjustPriceToForRentSource:(NSInteger)sourceId originPrice:(NSString *)price;
//不卖了
- (void)cancelForSaleSource:(NSInteger)sourceId;
//不租了
- (void)cancelForRentSource:(NSInteger)sourceId;
//已经出售
- (void)completeForSaleSource:(NSInteger)sourceId price:(NSString *)price;
//已经出租
- (void)completeForRentSource:(NSInteger)sourceId price:(NSString *)price;
//删除出售房源
- (void)deleteForSaleSource:(NSInteger)sourceId;
//删除出租房源
- (void)deleteForRentSource:(NSInteger)sourceId;

- (void)shouldUpdateList:(void(^)())update;

@end

为什么AppService只是由ViewModel弱引用,而不是强引用持有呢?我想了几点原因:
一,AppService常常需要引用到的View/Controller,若是ViewModel强引用了AppService就会强引用表现层的对象,这是不应该的。
二,AppService其实不仅仅设计给ViewModel用,它也常常是通用的组件,应该可以在很多地方脱离ViewModel而独立使用。
三,AppService其实就是ViewModel的代理,代理关系,要使用弱引用。

【Controller、View、ViewModel和AppService是如何分工合作?】

情形一,ViewModel只在当前Controller起作用。
这种情况,Controller拥有ViewModel和AppService,并负责初始化它们,通常是先初始化AppService,再用AppService初始化ViewModel。Controller强引用AppService和ViewModel;ViewModel弱引用AppService。Controller还负责把View和ViewModel恰当地关联起来,当View要用到什么数据时,就用ViewModel的属性或者方法赋值给它;当View响应了什么事件时,就执行ViewModel的响应处理方法;当ViewModel有什么数据更新时,让View做响应的刷新UI操作。AppService虽然可以被Controller直接使用,但由于Controller通常把大部分事情都交给ViewModel,所以实际情况应该是Controller基本不去使用AppService而是由ViewModel去使用它。当Controller被销毁时,View、ViewModel和AppService跟着销毁。
情形二,ViewModel可以执行导航操作
这种情况,除了满足情况一的特性之外,ViewModel还可以执行导航任务,也就是跳出本模块跟其他模块交互。大体是这样的:ControllerA拥有ViewModelA和AppServiceA,在初始化AppServiceA时就把导航控制器传给它,如下:

/// 初始化时给导航控制器,注意在.m文件里面弱引用
AppServiceA *service = [AppServiceA alloc] initWithNavigationController:(UINavigationController *)controller;
///或者属性赋值,navigationController是弱引用属性
service.navigationController = self.navigationController;

AppServiceA具有跳转的方法,可以有以下几种形式:

/// AppServiceA.h

/// 无参跳转
- (void)toControllerB;
/// 直接传递参数跳转
- (void)toControllerBWithPara1: para2: ... ;
/// 传递参数字典跳转
- (void)toControllerBWithParaDic:(NSDictionary *)paraDic;
///传递ViewModel跳转, 如果需要在ControllerA初始化ViewModelB,就可以采用这种方式。
- (void)toControllerBWithViewModelB:(ViewModelB *)viewModel;

AppServiceA导入了ControllerB和ViewModelB,所以它可以执行跳转。如果无法直接获取到ViewModelB,通常它便需要在跳转方法实体里面构建ViewModelB和ControllerB然后再执行push操作。跳到ControllerB也有几种情况,一,是要用ViewModelB来初始化它;二,是要将ViewModelB作为属性传给它;三,是它没有使用ViewModel,那就像一般跳转那样。

/// AppServiceA.m

#import "ViewModelB.h"
#import "ControllerB.h"
/// 传递参数字典跳转
- (void)toControllerBWithParaDic:(NSDictionary *)paraDic{
    id para1 = paraDic[para1];
    id para2 = paraDic[para2];

    // 需要ViewModelB来初始化
    ViewModelB *viewModel =[[ViewModelB alloc] init];
    viewModel.property1 = para1;
    viewModel.property2 = para2;
    ControllerB vc = [[ControllerB alloc] initWithViewModel:(ViewModelB *)viewModel];

    // 需要ViewModelB作为属性
    ViewModelB *viewModel =[[ViewModelB alloc] init];
    viewModel.property1 = para1;
    viewModel.property2 = para2;
    ControllerB vc = [[ControllerB alloc] init];
    vc.viewModel = viewModel;

    // 没有使用ViewModel
    ControllerB vc = [[ControllerB alloc] init];
    vc.property1 = para1;
    vc.property2 = para2;

    // push导航
    // self.navigationController push vc
}

拥有了AppServiceA的ControllerA它可以不直接管理任何它要跳转的Controller,因为那是AppServiceA,或者还有更多细小模块的其他AppService的职责,而ControllerA也不必了解怎么调用AppServiceA的方法去跳转,因为那是ViewModelA的职责。那么ViewModelA怎么去通过AppServiceA实现导航的呢?首先,ViewModelA拿到了AppServiceA的引用:

/// ViewModelA.h

// 初始化时就拿到了AppServiceA的引用
- (id)initWithAppService:(id<AppServiceA>)appService;
// 通过属性拿到了AppServiceA的引用
@property (nonatomic, weak) id <AppServiceA> appService;

/// 如果通过初始化拿到的引用,.m文件是这样的
/// ViewModelA.m

#import "AppServiceA.h"
@interface ViewModelA()
@property (nonatomic, weak) id <AppServiceA> appService;
@end
@implementation ViewModelA
- (id)initWithAppService:(id<AppServiceA>)appService{
    if (self = [super init]){
        self.appService = appService;
    }
    return self;
}
@end

然后,当ViewModelA实现实际需要触发导航的方法时,实际是通过调用AppServiceA的具备导航能力的相关方法来实现的。比如如果ViewModelA有如下方法:

/// ViewModelA.h

// 从列表项跳到详情页
- (void)toDetailsWithArticleId:(NSInteger)articleId;

/// ViewModelA.m

- (void)toDetailsWithArticleId: (NSInteger)articleId{
   [self.appService toDetailsWithArticleId: articleId];
}

情形三,ViewModel可以跨Controller操作。
有一些情形,一整套业务流程其实适合放在一个类里面统一处理,但UI设计却必须分步骤执行,这种时候ViewModel就适合起到跨Controller执行操作的作用。比如App有一个要用户填写个人资料的功能模块,用户要完整填写个人资料需要经过好几个页面的步骤,每个页面的数据都要缓存起来,因为,我们希望如果用户中途离开,当他下次再一步步进入这一流程时,在他每一步进入页面后,他填过数据的页面都有旧数据能够自动呈现。而且数据都要验证,验证的逻辑可能涉及到前面的数据,所以我们需要一个完整的上下文环境。另一方面,每个界面处理逻辑的代码有很多相同的地方,我们不希望每个页面都写一套几乎同样的代码。以下是一个展示这种情形的假想例子,它展示了程序需要分三个步骤给用户填写资料,每个步骤的界面几乎一样,逻辑大体相同,每个步骤的界面的状态需要根据前后步骤的状态做出相应改变。在这个例子里,ViewModel占据了实现这个功能的核心地位,Controller是受它支配的,在需要的时候Controller就被ViewModel通过AppService展示出来,用来显示信息和接受用户交互。这个功能的调用Controller只需要引入ViewModel并用它所需要的AppService初始化它,然后,后续的所有事情就交给ViewModel处理,其他细节它可以一概不管。功能入口代码:

//
//  ViewController.m
//  CrossControllersViewModelDemo

#import "ViewController.h"
#import "PersonalInfoAppServiceImpl.h"
#import "PersonalInfoViewModel.h"

@interface ViewController ()

@property (nonatomic, strong) PersonalInfoViewModel *viewModel;
@property (nonatomic, strong) PersonalInfoAppServiceImpl *appService;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupViewModel];
}

- (void)setupViewModel{
    self.appService = [[PersonalInfoAppServiceImpl alloc] initWithNavigationController:self.navigationController];
    self.viewModel = [[PersonalInfoViewModel alloc] initWithAppService:self.appService];
}

- (IBAction)toFillPersonalInfo:(id)sender {
    [self.viewModel toFillPersonalInfo];
}

@end

Controller只是简单的容器,它没有思考能力,只是被ViewModel操控,它只管直接从ViewModel那里拿到所有它需要展示的信息,它只管把从View那里响应到的交互交给ViewModel处理,它只管在ViewModel需要View更新某些界面时将最新数据交给View去更新显示。

//
//  PersonalInfoViewControllerA.m
//  CrossControllersViewModelDemo

#import "PersonalInfoViewControllerA.h"
#import "PersonalInfoContentView.h"
#import "Constants.h"

@interface PersonalInfoViewControllerA () <UITextFieldDelegate>

@property (nonatomic, weak) PersonalInfoViewModel *viewModel;
@property (nonatomic, weak) id <PersonalInfoAppService> appService;

@property (strong, nonatomic) PersonalInfoContentView *contentView;

@end

@implementation PersonalInfoViewControllerA

- (id)initWithViewModel:(PersonalInfoViewModel *)viewModel appService:(id<PersonalInfoAppService>)appService{
    @weakify(self);
    if (self = [super init]) {
        //初始化ViewModel和AppService
        self.appService = appService;
        self.viewModel = viewModel;
        //ViewModel状态的改变会导致View的改变
        [self.viewModel preStepStatus:^(BOOL enable) {
            @strongify(self);
            self.contentView.preBtn.enabled = enable;
        }];
        [self.viewModel nextStepStatus:^(BOOL enable, NSString *title) {
            @strongify(self);
            self.contentView.nextBtn.enabled = enable;
            [self.contentView.nextBtn setTitle:title forState:UIControlStateNormal];
        }];
        [self.viewModel completePercent:^(NSString *percent) {
            @strongify(self);
            self.contentView.percentLb.text = percent;
        }];
    }
    //用ViewModel的数据初始化View
    self.contentView = [[[NSBundle mainBundle] loadNibNamed:@"PersonalInfoContentViewXib" owner:nil options:nil] firstObject];
    self.contentView.frame = self.view.frame;
    self.contentView.percentLb.text = self.viewModel.completePercent;
    self.contentView.fistHeadLb.text = self.viewModel.firstTitle;
    self.contentView.secondHeadLb.text = self.viewModel.secondTitle;
    self.contentView.firstFieldTf.text = self.viewModel.firstField;
    self.contentView.secondFieldTf.text = self.viewModel.secondField;
    //View响应了交互事件转化为去执行ViewModel的处理交互事件的方法
    [self.contentView tapPreBtnAction:^{
        @strongify(self);
        [self.viewModel backToLastStep];
    }];
    [self.contentView tapNextBtnAction:^{
        @strongify(self);
        [self.viewModel goToNextStep];
    }];
    [self.contentView endEditingAction:^(NSString *firstField, NSString *secondField) {
        @strongify(self);
        [self.viewModel fillFirstField:firstField secondField:secondField];
    }];
    
    [self.view addSubview:self.contentView];
    self.title = self.viewModel.vcTitle;
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.viewModel loadData];
}

@end

ViewModel.h 最好设计的之暴露最少的属性和方法,保持易用、易理解,让下次在别处用到它时知道需要怎么去初始化它,可已从它那里获得什么数据,可以把什么交互交给他,可以怎么从它那里获得异步数据更新。

//
//  PersonalInfoViewModel.h
//  CrossControllersViewModelDemo

#import "PersonalInfoAppService.h"

typedef NS_ENUM(NSUInteger, StepType){
    StepTypeFirst = 1,
    StepTypeSecond,
    StepTypeThird
};

@interface PersonalInfoViewModel : NSObject
//为控制器内的views提供数据,一般是已读
@property (nonatomic, copy, readonly) NSString *vcTitle;
@property (nonatomic, copy, readonly) NSString *completePercent;
@property (nonatomic, copy, readonly) NSString *firstTitle;
@property (nonatomic, copy, readonly) NSString *secondTitle;
@property (nonatomic, copy, readonly) NSString *firstField;
@property (nonatomic, copy, readonly) NSString *secondField;

//它只依赖于一个PersonalInfoAppService实现类,这个service实现类负责去做具体的事情,比较典型的是获取远程数据,获取本地数据,存储本地数据,导航操作,弹框提醒
//这些功能对ViewModel来说就是一个个功能服务,它不需要理会它们怎么做到的,只需要在它需要它们的时候,叫AppService帮他做即可,它把焦点集中在它本身负责的功能的逻辑上。
- (id)initWithAppService:(id<PersonalInfoAppService>)service;
//下面这些方法响应了界面所需要响应的各种交互,比如检测数据状态、进入执行步骤、点击上一步、下一步等
- (void)loadData;
- (void)toFillPersonalInfo;
- (void)backToLastStep;
- (void)goToNextStep;
//这个方法是专门针对与内容展示的PersonalInfoContentView的交互而设计的,这个View每次在点击空白处时,告诉外界它刷新一下自己的数据,这些数据有两个textFields来
//展示,这个方法可以在每次View更新数据时,通知ViewModel更新响应的数据,然后让ViewModel状态发生响应的改变。这个方法导致ViewModel的实现跟View的实现方案有关联,
//我写的大部分ViewModel为了能更好服务于View,其实都多多少少有些受View的影响,如果要让ViewModel更通用,我们得认真设计这种交互方法,让它符合更一般性的用于于View的交互方案。只是我觉得,哪怕不是很通用的ViewModel也是很有意义的,它把功能脱离Controller和View封装起来了,保证了自身包含了所有内聚逻辑,以后挪到别处取用也方便。
- (void)fillFirstField:(NSString *)firstField secondField:(NSString *)secondField;
//下面这些方法是ViewModel状态改变时要异步通知相关的View也做出改变,在这种应用场景中,我比较喜欢用block而不是KVO或协议,代码能集中管理让人愉快。
- (void)preStepStatus:(void(^)(BOOL enable))statusUpdate;
- (void)nextStepStatus:(void(^)(BOOL enable, NSString *title))statusUpdate;
- (void)completePercent:(void(^)(NSString *percent))statusUpdate;

@end

ViewModel.m 里面实现了一切跟这个功能相关的逻辑,并不是说它做了一切事情,那样的话它就是一个全能类了,不靠谱,它有很多事情是代理给AppService去做的。那么哪些是该由它做的事情呢?在这个例子里,ViewModel做的事情就是:一,恰如其分地划分资料填写步骤,规划好哪个步骤填写什么信息;二,做好对信息的校验;三,根据填写的信息和所处的步骤设置每个环节的各种状态,比如能不能点击上一步、下一步的按钮,能不能添加进度;四,保持已有的数据和状态,让退出后再进来能够正确地预填数据和预设状态等等。ViewModel的逻辑通常是最复杂的,所以ViewModel的类的代码量如果不经过精心的拆分会容易变得比较重。不过如果写的好,哪怕代码量大一点,只要写好一次,后面再用这个功能时就方便了,总好过将整个功能的逻辑散落在Congtroller,View和一些Manager,然后下次如果要用这个功能,但又不想用这些Controller和View时,将会面临很蛋疼的逻辑拆分、重组、重新测试的工作量。好不容易、费劲脑汁写好的逻辑,还是让它能够完整地被移植、被重用比较好,比较再写容易出错,这时候就适合把它封装到ViewModel里面。

//
//  PersonalInfoViewModel.m
//  CrossControllersViewModelDemo

#import "PersonalInfoViewModel.h"

typedef NS_ENUM(NSUInteger, FieldType){
    FieldTypeName = 1,
    FieldTypeSex,
    FieldTypeAge,
    FieldTypeAddress,
    FieldTypePhone,
    FieldTypeID
};

typedef NS_ENUM(NSUInteger, FieldCompleteType){
    FieldCompleteTypeName = 1,
    FieldCompleteTypeSex = 1 << 1,
    FieldCompleteTypeAge = 1 << 2,
    FieldCompleteTypeAddress = 1 << 3,
    FieldCompleteTypePhone = 1 << 4,
    FieldCompleteTypeID = 1 << 5
};

typedef NS_ENUM(NSUInteger, StepCompleteType){
    StepCompleteTypeFirst = 1 ,
    StepCompleteTypeSecond = 1 << 1,
    StepCompleteTypeThird = 1 << 2
};

@interface PersonalInfoViewModel()

@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy, readonly) NSString *sex;
@property (nonatomic, copy, readonly) NSString *age;
@property (nonatomic, copy, readonly) NSString *address;
@property (nonatomic, copy, readonly) NSString *phone;
@property (nonatomic, copy, readonly) NSString *ID;

@property (weak, nonatomic) id <PersonalInfoAppService> appService;
@property (copy, nonatomic) void (^preNavStatusBlock)(BOOL);
@property (copy, nonatomic) void (^nextNavStatusBlock)(BOOL, NSString*);
@property (copy, nonatomic) void (^percentStatusBlock)(NSString*);
@property (assign, nonatomic) NSUInteger fieldCompleteStatus;
@property (assign, nonatomic) NSUInteger stepCompleteStatus;

@property (nonatomic, assign) StepType currentStep;

@end

@implementation PersonalInfoViewModel

#pragma mark - Public

- (id)initWithAppService:(id)service{
    if (self = [super init]) {
        self.appService = service;
        self.fieldCompleteStatus = 0;
        self.currentStep = StepTypeFirst;
    }
    return self;
}
//由于ViewModel本身具有上下文数据环境,和完整的检测规则,这个方法才能够轻易地在每一个步骤的页面加载时和数据变更时被调用来为View的正确显示做恰当的检查
- (void)loadData{
    [self changeCompletePercent];
    [self changeNavigationStatusWithStep:self.currentStep];
}
- (void)toFillPersonalInfo{
    self.currentStep = StepTypeFirst;
    [self.appService toNextStepWithViewModel:self];
}
- (void)backToLastStep{
    if (self.currentStep < StepTypeSecond) {
        return;
    }
    self.currentStep --;
    [self.appService navBack];
}
- (void)goToNextStep{
    if (self.currentStep > StepTypeThird) {
        return;
    }
    if (self.stepCompleteStatus == [self stepThirdCompleteStatus]) {
        [self.appService saveViewModel:self];
    }
    self.currentStep ++;
    [self.appService toNextStepWithViewModel:self];
}

- (void)preStepStatus:(void (^)(BOOL))statusUpdate{
    self.preNavStatusBlock = statusUpdate;
}
- (void)nextStepStatus:(void (^)(BOOL, NSString *))statusUpdate{
    self.nextNavStatusBlock = statusUpdate;
}
- (void)fillName:(NSString *)name{
    _name = name;
    [self changeFiledType:FieldTypeName withCondition:[self checkFieldType:FieldTypeName field:_name]];
}
- (void)fillSex:(NSString *)sex{
    _sex = sex;
    [self changeFiledType:FieldTypeSex withCondition:[self checkFieldType:FieldTypeSex field:_sex]];
}
- (void)fillAge:(NSString *)age{
    _age = age;
    [self changeFiledType:FieldTypeAge withCondition:[self checkFieldType:FieldTypeAge field:_age]];
}
- (void)fillAddress:(NSString *)address{
    _address = address;
    [self changeFiledType:FieldTypeAddress withCondition:[self checkFieldType:FieldTypeAddress field:_address]];
}
- (void)fillPhone:(NSString *)phone{
    _phone = phone;
    [self changeFiledType:FieldTypePhone withCondition:[self checkFieldType:FieldTypePhone field:_phone]];
}
- (void)fillID:(NSString *)ID{
    _ID = ID;
    [self changeFiledType:FieldTypeID withCondition:[self checkFieldType:FieldTypeID field:_ID]];
}
- (void)completePercent:(void (^)(NSString *))statusUpdate{
    self.percentStatusBlock = statusUpdate;
}

- (void)fillFirstField:(NSString *)firstField secondField:(NSString *)secondField{
    if (self.currentStep == StepTypeFirst) {
        [self fillName:firstField];
        [self fillSex:secondField];
    }else if (self.currentStep == StepTypeSecond){
        [self fillAge:firstField];
        [self fillAddress:secondField];
    }else if (self.currentStep == StepTypeThird){
        [self fillPhone:firstField];
        [self fillID:secondField];
    }
    [self loadData];
}

#pragma mark - Properties

- (NSString *)vcTitle{
    if (self.currentStep == StepTypeFirst) {
        return @"第一步";
    }else if (self.currentStep == StepTypeSecond){
        return @"第二步";
    }else{
        return @"第三步";
    }
}

- (NSUInteger)stepCompleteStatus{
    return [self stepCompleteStatusWithFieldCompleteStatus:self.fieldCompleteStatus];
}

- (NSString *)completePercent{
    NSUInteger completePercent = [self completePercentWithStepCompleteStatus:self.stepCompleteStatus];
    return [NSString stringWithFormat:@"完成:%@%%",@(completePercent)];
}

- (NSString *)firstTitle{
    if (self.currentStep == StepTypeFirst) {
        return @"姓名";
    }else if (self.currentStep == StepTypeSecond){
        return @"年龄";
    }else{
        return @"手机";
    }
}

- (NSString *)secondTitle{
    if (self.currentStep == StepTypeFirst) {
        return @"性别";
    }else if (self.currentStep == StepTypeSecond){
        return @"地址";
    }else{
        return @"身份证";
    }
}

- (NSString *)firstField{
    if (self.currentStep == StepTypeFirst) {
        return self.name;
    }else if (self.currentStep == StepTypeSecond){
        return self.age;
    }else{
        return self.phone;
    }
}

- (NSString *)secondField{
    if (self.currentStep == StepTypeFirst) {
        return self.sex;
    }else if (self.currentStep == StepTypeSecond){
        return self.address;
    }else{
        return self.ID;
    }
}

#pragma mark - Navigation

- (void)changeNavigationStatusWithStep:(NSUInteger)step{
    switch (step) {
        case StepTypeFirst:{
            [self setPreNavEnable:NO];
            if (StepCompleteTypeFirst & self.stepCompleteStatus) {
                [self setNextNavEnable:YES title:@"下一步"];
            }else{
                [self setNextNavEnable:NO title:@"下一步"];
            }
        }
            break;
        case StepTypeSecond:{
            [self setPreNavEnable:YES];
            if (StepCompleteTypeSecond & self.stepCompleteStatus) {
                [self setNextNavEnable:YES title:@"下一步"];
            }else{
                [self setNextNavEnable:NO title:@"下一步"];
            }
        }
            break;
        case StepTypeThird:{
            [self setPreNavEnable:YES];
            if (StepCompleteTypeThird & self.stepCompleteStatus) {
                [self setNextNavEnable:YES title:@"完成"];
            }else{
                [self setNextNavEnable:NO title:@"下一步"];
            }
        }
            break;
        default:
            break;
    }
}

- (void)setPreNavEnable:(BOOL)enable{
    if (self.preNavStatusBlock) {
        self.preNavStatusBlock(enable);
    }
}

- (void)setNextNavEnable:(BOOL)enable title:(NSString *)title{
    if (self.nextNavStatusBlock) {
        self.nextNavStatusBlock(enable, title);
    }
}

#pragma mark - Check Data

- (BOOL)checkFieldType:(FieldType)type field:(NSString *)field{
    if (field.length == 0) {
        return NO;
    }
    switch (type) {
        case FieldTypeName:{
            
        }
            break;
        case FieldTypeSex:{
            
        }
            break;
        case FieldTypeAge:{
            
        }
            break;
        case FieldTypeAddress:{
            
        }
            break;
        case FieldTypePhone:{
            
        }
            break;
        case FieldTypeID:{
            
        }
            break;
        default:
            break;
    }
    return YES;
}

#pragma mark - Change Status

- (void)changeCompletePercent{
    if (self.percentStatusBlock) {
        self.percentStatusBlock(self.completePercent);
    }
}

- (void)changeFiledType:(FieldType)type withCondition:(BOOL)condition{
    if (condition) {
        [self finishFieldType:type];
    }else{
        [self unfinishFieldType:type];
    }
}

- (void)unfinishFieldType:(FieldType)type{
    switch (type) {
        case FieldTypeName:{
            self.fieldCompleteStatus &= ~FieldCompleteTypeName;
        }
            break;
        case FieldTypeSex:{
            self.fieldCompleteStatus &= ~FieldCompleteTypeSex;
        }
            break;
        case FieldTypeAge:{
            self.fieldCompleteStatus &= ~FieldCompleteTypeAge;
        }
            break;
        case FieldTypeAddress:{
            self.fieldCompleteStatus &= ~FieldCompleteTypeAddress;
        }
            break;
        case FieldTypePhone:{
            self.fieldCompleteStatus &= ~FieldCompleteTypePhone;
        }
            break;
        case FieldTypeID:{
            self.fieldCompleteStatus &= ~FieldCompleteTypeID;
        }
            break;
        default:
            break;
    }
}

- (void)finishFieldType:(FieldType)type{
    switch (type) {
        case FieldTypeName:{
            self.fieldCompleteStatus |= FieldCompleteTypeName;
        }
            break;
        case FieldTypeSex:{
            self.fieldCompleteStatus |= FieldCompleteTypeSex;
        }
            break;
        case FieldTypeAge:{
            self.fieldCompleteStatus |= FieldCompleteTypeAge;
        }
            break;
        case FieldTypeAddress:{
            self.fieldCompleteStatus |= FieldCompleteTypeAddress;
        }
            break;
        case FieldTypePhone:{
            self.fieldCompleteStatus |= FieldCompleteTypePhone;
        }
            break;
        case FieldTypeID:{
            self.fieldCompleteStatus |= FieldCompleteTypeID;
        }
            break;
        default:
            break;
    }
}

- (NSUInteger)stepCompleteStatusWithFieldCompleteStatus:(NSUInteger)filedCompleteStatus{
    NSUInteger finishedFieldCount = 0;
    NSUInteger status = 0;
    for (int i = 0; i < 6 ; i++) {
        if(filedCompleteStatus & 1 << i){
            finishedFieldCount ++;
        }
    }
    if (finishedFieldCount >= 6 && self.currentStep >= StepTypeThird) {
        status = [self stepThirdCompleteStatus];
    }else if (finishedFieldCount >= 4 && self.currentStep >= StepTypeSecond){
        status = [self stepSecondCompleteStatus];
    }else if (finishedFieldCount >= 2 && self.currentStep >= StepTypeFirst){
        status = [self stepFirstCompleteStatus];
    }
    return status;
}

- (NSUInteger)completePercentWithStepCompleteStatus:(NSUInteger)stepCompleteStatus{
    NSUInteger percent = 0;
    if (stepCompleteStatus == [self stepFirstCompleteStatus] && self.currentStep >= StepTypeFirst) {
        percent = 33;
    }else if (stepCompleteStatus == [self stepSecondCompleteStatus] && self.currentStep >= StepTypeSecond){
        percent = 66;
    }else if (stepCompleteStatus == [self stepThirdCompleteStatus] && self.currentStep >= StepTypeThird){
        percent = 100;
    }
    return percent;
}

- (NSUInteger)stepFirstCompleteStatus{
    return StepCompleteTypeFirst;
}

- (NSUInteger)stepSecondCompleteStatus{
    return StepCompleteTypeFirst | StepCompleteTypeSecond;
}

- (NSUInteger)stepThirdCompleteStatus{
    return StepCompleteTypeFirst | StepCompleteTypeSecond | StepCompleteTypeThird;
}

@end

下面是demo运行效果图:

第一步.jpeg
第二步.jpeg
第三步.jpeg

例子演示的是分三步,也可以分四步、五步,那时就要修改ViewModel了,而且应该差不多只需要修改ViewModel就可以了,如果UI布局不需要做太大改变的话。为了方便只用一个Controller就能做这么多个步骤的展示,可以通过对View进行良好的封装,让它更容易使得ViewModel实现灵活操控Controller来展示不同步骤信息的目的。比如这个
PersonalInfoContentView:

#import <UIKit/UIKit.h>

@interface PersonalInfoContentView : UIView

@property (weak, nonatomic) IBOutlet UILabel *percentLb;
@property (weak, nonatomic) IBOutlet UILabel *fistHeadLb;
@property (weak, nonatomic) IBOutlet UITextField *firstFieldTf;
@property (weak, nonatomic) IBOutlet UILabel *secondHeadLb;
@property (weak, nonatomic) IBOutlet UITextField *secondFieldTf;
@property (weak, nonatomic) IBOutlet UIButton *preBtn;
@property (weak, nonatomic) IBOutlet UIButton *nextBtn;

- (void)tapPreBtnAction:(void(^)())action;
- (void)tapNextBtnAction:(void(^)())action;
- (void)endEditingAction:(void(^)(NSString *firstField, NSString *secondField))action;

@end

#import "PersonalInfoContentView.h"

@interface PersonalInfoContentView()

@property (copy, nonatomic) void (^preBtnAction)();
@property (copy, nonatomic) void (^nextBtnAction)();
@property (copy, nonatomic) void (^endEditingAction)(NSString *, NSString *);

@end

@implementation PersonalInfoContentView

- (void)tapPreBtnAction:(void (^)())action{
    self.preBtnAction = action;
}

- (void)tapNextBtnAction:(void (^)())action{
    self.nextBtnAction = action;
}

- (void)endEditingAction:(void (^)(NSString *, NSString *))action{
    self.endEditingAction = action;
}

- (IBAction)tapPreBtn:(id)sender {
    if (self.preBtnAction) {
        self.preBtnAction();
    }
}
- (IBAction)tapNextBtn:(id)sender {
    if (self.nextBtnAction) {
        self.nextBtnAction();
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    UIView *touchedView = [[touches anyObject] view];
    if (![touchedView isKindOfClass:[UITextField class]]) {
        [self.firstFieldTf resignFirstResponder];
        [self.secondFieldTf resignFirstResponder];
        if (self.endEditingAction) {
            self.endEditingAction(self.firstFieldTf.text,self.secondFieldTf.text);
        }
    }
}

@end

PersonalInfoContentView的Xib文件:


PersonalInfoContentViewXib.jpeg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容