RAC&MVVM来吧,搞个小闹钟(iOS)
写在文章之前
清明回来,跟朋友去健身房,朋友说iOS无法设置法定工作日闹钟(即法定节日不提醒),弄得好烦,由于忘记调闹钟,假期被叫醒了好几次。于是我拿过朋友手机看了一下还真没有。
iPhone大概是这样。

安卓大概是这样。

不要问我为什么要看朋友手机,其实,我就是那个做iOS开发没有iPhone的开发人员。
言归正传,没有就搞搞呗。
框架依旧是MVVM,正好来巩固一下。
开始之前的思考
我如何保存闹钟的数据,比如名字,闹钟时间,重复类型等,放到哪里呢?网络接口肯定用不到,那就本地存储呗,数据并不多,简单的来个plist吧,用过之后我才发现这是有多坑。这是后话,所以说,开始前的思考还是很重要的。
开始吧
先看一下目录结构。

View
view使用的很简单,AllClock中就用到了一个tableview,EditClock界面也不是很复杂。大概就是这个样子。

这里我们说一个使用RAC比较方便的地方。
YRAllClockTableViewCell.m中
//返回触发switch后on状态改变来改变Label颜色
-(RACSignal *)switchOnColorSignal{
@weakify(self);
return [[self.statusSwitch rac_signalForControlEvents:UIControlEventValueChanged]map:^id(UISwitch *value) {
@strongify(self);
self.isOpen = value.on;
return value.on ? kGrayColor(184, 1) : kGrayColor(232, 1);
}];
}
//返回触发switch后on状态改变来改变timeLabel颜色
-(RACSignal *)switchOnTimeLbColorSignal{
return [[self.statusSwitch rac_signalForControlEvents:UIControlEventValueChanged]map:^id(UISwitch *value) {
[self.switchSubject sendNext:@(value.on)];
return value.on ? kGrayColor(0, 1) : kGrayColor(232, 1);
}];
}
这两个方法通过监听statusSwitch的UIControlEventValueChanged Event来返回对应的颜色。
然后在SetView中我们进行绑定。就像这样。
//通过RAC来绑定
RAC(self.clockNameLb,textColor) = [self switchOnColorSignal];
RAC(self.typeLb,textColor) = [self switchOnColorSignal];
RAC(self.statusLb,textColor) = [self switchOnColorSignal];
RAC(self.timeLb,textColor) = [self switchOnTimeLbColorSignal];
看一下四句话的效果。

数据处理
既然使用的是plist那么,对于plist的使用就比较麻烦了,还好stackoverflow上有个很不错的答案。这是网址,大家可以一起学习。
http://stackoverflow.com/questions/23867337/how-can-i-save-retrieve-delete-update-my-data-in-plist-file-in-ios
由于数据是唯一的,所以这里我们使用单例来绑定数据,这样数据的增删改都是与单例同步,我们使用的时候直接调用单例即可。
下面来详细说一下DataSingleton这个类。
.h需要公开的有四个接口,一个变量。接口用来对plist进行增删改,变量返回的就是需要的数据了。
@interface DataSingleton: NSObject
+ (instancetype)sharedInstance;
//数据array
@property(nonatomic,strong)NSArray *dataArray;
/**
* 添加数据
*
* @param clockModel 添加的数据
*/
-(void)insertClockListData:(YRClockModel *)clockModel;
/**
* 删除数据
*
* @param row 删除第几行
*/
-(void)removeClockListData:(NSInteger)row;
/**
* 更新数据
*
* @param row 更新第几行
* @param clockModel 更新的数据
*/
-(void)updateClockListDataWithRow:(NSInteger)index andModel:(YRClockModel *)clockModel;
//更新clockData
-(void)updateClockDataArray;
@end
.m中我们一个一个来看。
首先,不管增删改查,都需要一个东西,就是获取到plist的位置,拿到plist。于是有了这个方法。
//获取地址
-(NSString *)getFilePath{
NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *plistPath = [documentsPath stringByAppendingPathComponent:@"ClockList.plist"];
return plistPath;
}
接下来就是查这个操作。
//获取数据并转换成model返回
-(NSArray *)getClockListData{
//如果不存在这个文件,返回一个空的数组即可
if (![[NSFileManager defaultManager] fileExistsAtPath:self.plistPath]){
return [NSArray array];
}else{
NSDictionary *dict = [[NSDictionary alloc] initWithContentsOfFile:self.plistPath];
NSArray *dataArr = dict[@"data"];
//如果dataArr不为空说明数据存在,那么处理数据,如果为空,则返回空的数组。
if (dataArr.count != 0) {
NSMutableArray *array = [NSMutableArray array];
for (NSDictionary *dic in dataArr) {
YRClockModel *clockModel = [[YRClockModel alloc]init];
[clockModel setValuesForKeysWithDictionary:dic];
[array addObject:clockModel];
}
//从这里我们也不难发现,我们返回的是一个包含多个YRClockModel对象的一个数组,使用时直接使用YRClockModel即可。
return [array copy];
}else{
return [NSArray array];
}
}
}
//我们在init方法中需要对self.data赋值,然后才能下面增删改的操作。
-(instancetype)init{
self = [super init];
if (self) {
self.plistPath = [self getFilePath];
self.dataArray = [self getClockListData];
}
return self;
}
查询完成计时增这个操作
//添加数据
-(void)insertClockListData:(YRClockModel *)clockModel{
NSDictionary *plistDic = [[NSDictionary alloc] initWithContentsOfFile:self.plistPath];
//如果为空说明没有plist文件,这里要先创建文件
if (plistDic == nil){
plistDic = [[NSDictionary alloc] initWithObjects:@[[NSMutableArray array]] forKeys:@[@"data"]];
}
[[plistDic objectForKey:@"data"] insertObject:[self setValueWithMdoel:clockModel] atIndex:self.dataArray.count];
NSError *error = nil;
NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:plistDic format:NSPropertyListXMLFormat_v1_0 options:0 error:&error];
NSLog(@"%@",error);
if(plistData){
[plistData writeToFile:self.plistPath atomically:YES];
NSLog(@"Data saved sucessfully");
//添加成功后,我们需要及时的对dataArray进行更新。
self.dataArray = [self getClockListData];
[self updateClockDataArray];
}
else{
NSLog(@"Data not saved");
}
}
因为insert与update都需要对数据进行赋值,所以我们这里将其封装成一个方法,方便后面使用。
//model转Dic处理
-(NSMutableDictionary *)setValueWithMdoel:(YRClockModel *)clockModel{
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
[dic setObject:clockModel.clockName forKey:@"clockName"];
[dic setObject:clockModel.clockTime forKey:@"clockTime"];
[dic setObject:@(clockModel.clockType) forKey:@"clockType"];
[dic setObject:@(clockModel.isOpen) forKey:@"isOpen"];
[dic setObject:clockModel.clockRing forKey:@"clockRing"];
[dic setObject:@(clockModel.clockRemindType) forKey:@"clockRemindType"];
[dic setObject:@(clockModel.clockVolume) forKey:@"clockVolume"];
return dic;
}
接下里就是改。
这里说下为什么plist会很麻烦呢,改的时候我们会发现,我们需要先将修改那一行删掉,然后在在那一行添加上新的数据,即使你只修改的,也需要删除那一行所有的数据。
//更新数据
-(void)updateClockListDataWithRow:(NSInteger)index andModel:(YRClockModel *)clockModel{
NSDictionary *plistDic = [[NSDictionary alloc] initWithContentsOfFile:self.plistPath];
[[plistDic objectForKey:@"data"] removeObjectAtIndex:index];
[[plistDic objectForKey:@"data"] insertObject:[self setValueWithMdoel:clockModel] atIndex:index];
NSError *error = nil;
NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:plistDic format:NSPropertyListXMLFormat_v1_0 options:0 error:&error];
NSLog(@"%@",error);
if(plistData){
[plistData writeToFile:self.plistPath atomically:YES];
NSLog(@"Data saved sucessfully");
//更新成功后,我们需要及时的对dataArray进行更新。
self.dataArray = [self getClockListData];
[self updateClockDataArray];
}
else{
NSLog(@"Data not saved");
}
}
最后的删除
//删除数据
-(void)removeClockListData:(NSInteger)row{
NSDictionary *plistDic = [[NSDictionary alloc] initWithContentsOfFile:self.plistPath];
[[plistDic objectForKey:@"data"] removeObjectAtIndex:row];
NSError *error = nil;
NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:plistDic format:NSPropertyListXMLFormat_v1_0 options:0 error:&error];
NSLog(@"%@",error);
if(plistData){
[plistData writeToFile:self.plistPath atomically:YES];
NSLog(@"Data saved sucessfully");
//删除成功后,我们需要及时的对dataArray进行更新。
self.dataArray = [self getClockListData];
[self updateClockDataArray];
}
else{
NSLog(@"Data not saved");
}
}
OK,到此为止我们对数据的处理算是完成了。
单例ClockSingleton,这个单例存放的dataArray是用来存储当天需要响铃的时间。于是我们可以看到在DataSingleton增删改的时候有一个updateClockDataArray方法来更新ClockSingleton。
单例HolidaySingleton,用于存放今年放假的日期。
Model处理
再来看一下Model。Model也不是很复杂,但是开始的时候让我想复杂了。
说一下我当时是怎么想的。闹钟必备三个因素,重复类型,时间,名称,于是想把它们放到BaseModel里吧。All界面会用到一个是否打开的字段,于是想建一个类,然后继承BaseModel在里面添加上isOpen这个字段。Edit界面会用到铃声,音量等字段。于是想创建一个类,继承自BaseModel,添加clockRing,clockVolume等。但是,之前说过,update的时候需要删了重新添加,你会发现你需要所有的字段。这样反而麻烦,所以BaseModel不动,其他的放到一个类里了。
总结原因,咨询了一下公司的后台,使用Sql可以解决,还是初步设计有问题,其次,还要加强一下sql的使用。
于是BaseModel.h,.m中没有东西
@interface YRBaseModel : NSObject
//闹钟名
@property(nonatomic,copy)NSString *clockName;
//闹钟时间
@property(nonatomic,copy)NSString *clockTime;
//闹钟类型
@property(nonatomic,assign)CLOCKTYPE clockType;
@end
ClockModel.h, .m中没有东西
@interface YRClockModel : YRBaseModel
//闹钟开关状态
@property(nonatomic,assign)BOOL isOpen;
//闹钟铃声
@property(nonatomic,copy)NSString *clockRing;
//闹钟音量
@property(nonatomic,assign)NSInteger clockVolume;
//闹钟延迟提醒时间
@property(nonatomic,assign)CLOCKREMINDTYPE clockRemindType;
@end
View,Model算是处理完了。还有两个大头,一个就是ViewModel,一个就是Controller了。
刚才看了一下,我们最好先来看看ViewModel
ViewModel
由于上面的原因,你会发现有一个BaseViewModel,有一个ClockViewModel,一样ClockViewModel继承自BaseViewModel。
看一下BaseViewModel.h
@interface YRBaseViewModel : NSObject
/**
* 获取数据,通过index来获取要处理第几个数据
*
* @param index cell.indexPath.row
*/
-(void)getClockDataWithIndex:(NSInteger)index;
//闹钟名字
@property(nonatomic,copy)NSString *clockName;
//闹钟类型
@property(nonatomic,copy)NSString *clockType;
//闹钟时间
@property(nonatomic,copy)NSString *clockTime;
//数据
@property(nonatomic,strong)DataSingleton *data;
//当前cell的model
@property(nonatomic,strong)YRClockModel *model;
@end
BaseViewModel.m 来看下getClockDataWithIndex:方法
-(void)getClockDataWithIndex:(NSInteger)index{
if (index != -1) {
self.model = self.data.dataArray[index];
self.clockName = self.model.clockName;
switch (self.model.clockType) {
case CLOCKTYPEHOLIDAYS:
self.clockType = @"法定节假日";
break;
case CLOCKTYPEWORKDAYS:
self.clockType = @"周一到周五";
break;
case CLOCKTYPEEVERYDAYS:
self.clockType = @"每天";
break;
case CLOCKTYPEJUSTONETIME:
self.clockType = @"仅一次";
break;
default:
break;
}
self.clockTime = self.model.clockTime;
}else{
self.clockName = @"闹钟";
self.clockType = @"仅一次";
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"HH:mm"];
self.clockTime = [formatter stringFromDate:[NSDate date]];
}
}
由于我们类型是通过枚举存储,所以这里需要将其转换成对应的字符串,返回使用。为什么会有个-1的判断呢,这里考虑到如果是新建,我们就无法通过index来获取到对应的model,所以我们通过-1来区分是新建还是获取对应的值,同时新建的时候我们会赋初值。
-(instancetype)init{
self = [super init];
if (self) {
self.data = [DataSingleton sharedInstance];
}
return self;
}
还有一个方法就是init方法,进行对data进行赋值。
接下来就看一下ClockViewModel.h
@interface YRClockViewModel : YRBaseViewModel
/**
* 获取简单的数据,即Cell中的数据
*
* @param index cell.indexPath.row
*/
-(void)getClockSimpleDataWithIndex:(NSInteger)index;
//闹钟开关状态
@property(nonatomic,assign)BOOL isOpen;
//闹钟铃声
@property(nonatomic,copy)NSString *clockRing;
//闹钟提醒间隔时间
@property(nonatomic,copy)NSString *clockRemindType;
/**
* 闹钟音量(0 - 15)
*/
@property(nonatomic,assign)NSInteger clockVolume;
//保存按钮事件
@property(nonatomic,strong)RACCommand *saveCommand;
//删除按钮事件
@property(nonatomic,strong)RACCommand *deleteCommand;
//用于数据存储时,绑定CLOCKTYPE
@property(nonatomic,assign)CLOCKTYPE clockModelType;
//用于数据存储时,绑定CLOCKREMINDTYPE
@property(nonatomic,assign)CLOCKREMINDTYPE clockModelRemindType;
//用来监听是否打开,来改值
@property(nonatomic,strong)RACSignal *openSignal;
//由于闹钟开关会影响到lb的颜色,这里返回颜色
@property(nonatomic,strong)UIColor *color;
//由于闹钟开关会影响到timeLb的颜色,这里返回颜色
@property(nonatomic,strong)UIColor *timeColor;
@end
重写父类的getClockDataWithIndex:方法
-(void)getClockDataWithIndex:(NSInteger)index{
[super getClockDataWithIndex:index];
if (index != -1) {
self.clockRing = self.model.clockRing;
switch (self.model.clockRemindType) {
case CLOCKREMINDTYPENONE:
self.clockRemindType = @"关闭";
break;
case CLOCKREMINDTYPEFIVE:
self.clockRemindType = @"5分钟";
break;
case CLOCKREMINDTYPETEN:
self.clockRemindType = @"10分钟";
break;
case CLOCKREMINDTYPEFIFTEEN:
self.clockRemindType = @"15分钟";
break;
case CLOCKREMINDTYPETHIRTY:
self.clockRemindType = @"30分钟";
break;
default:
break;
}
self.clockVolume = self.model.clockVolume;
self.deleteCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(NSNumber *input) {
@weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
[self.data removeClockListData:input.integerValue];
return [RACDisposable disposableWithBlock:^{
}];
}];
}];
}else{
self.clockRing = @"lalala";
self.clockRemindType = @"5分钟";
self.clockVolume = 10;
}
self.saveCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(NSNumber *input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
YRClockModel *model = [[YRClockModel alloc]init];
model.clockName = self.clockName;
model.clockVolume = self.clockVolume;
model.clockRing = self.clockRing;
model.clockType = self.clockModelType;
model.clockRemindType = self.clockModelRemindType;
model.clockTime = self.clockTime;
model.isOpen = YES;
@weakify(self);
if (input.integerValue != -1) {
@strongify(self);
[self.data updateClockListDataWithRow:index andModel:model];
NSLog(@"%@",input);
}else{
@strongify(self);
[self.data insertClockListData:model];
}
return [RACDisposable disposableWithBlock:^{
}];
}];
}];
}
使用-1的原因与上面一样
这里有个需要细说的一个地方。saveCommand与deleteCommand,command也是这次才会用,才理解的。
我们使用RAC下的UIButton时不难发现有个属性rac_command,command有个很方便的地方就是,如果你这次点击事件没有处理完,多次点击是无效的。这可以解决很多多动症用户狂点按钮多次发送网络请求的问题。
self.deleteCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(NSNumber *input) {
@weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
[self.data removeClockListData:input.integerValue];
return [RACDisposable disposableWithBlock:^{
}];
}];
}];
这段话只是给deleteCommand赋值,并没有执行command,如何执行command,我们需要调用其一个方法
[self.viewModel.deleteCommand execute:(你需要传递的值,如果没有写nil即可)];
.m中还有个方法需要说明一下,就是getClockSimpleDataWithIndex:这个方法。
-(void)getClockSimpleDataWithIndex:(NSInteger)index{
[super getClockDataWithIndex:index];
self.clockModelType = self.model.clockType;
self.isOpen = self.model.isOpen;
self.color = self.model.isOpen ? kGrayColor(184, 1):kGrayColor(232, 1);
self.timeColor = self.model.isOpen ? kGrayColor(0, 1):kGrayColor(232, 1);
}
为什么需要这个方法呢,因为我们不难发现,我们的AllViewController中只需要一个isOpen参数即可。所以,我们可以使用getClockSimpleDataWithIndex:这个方法即可。
Controller
最后就剩下Controller了,过上性福生活就靠它了,额。。不对,连接View与ViewModel就靠它了。
AllViewController
重点:AllViewController中有三个比较重要的方法,其他的都比较中规中矩
第一个bindingViewModel
-(void)bindingViewModel{
self.viewModel = [[YRClockViewModel alloc]init];
dispatch_async(dispatch_get_main_queue(), ^{
[self.allClockTableView reloadData];
});
self.timeSignal = [[[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] filter:^BOOL(NSDate *value) {
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"ss"];
return [formatter stringFromDate:value].integerValue == 0;
}] map:^NSString *(NSDate * date) {
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"HH:mm"];
return [formatter stringFromDate:date];
}];
@weakify(self);
[self.timeSignal subscribeNext:^(NSString *x) {
if ([x isEqualToString:@"00:00"]) {
[[DataSingleton sharedInstance] updateClockDataArray];
}
for (RACTuple *tuple in [ClockSingleton sharedInstance].dataArray) {
@strongify(self);
NSString *time = tuple.first;
if ([x isEqualToString:time]) {
NSNumber *i = tuple.second;
NSNumber *type = tuple.third;
if (type.integerValue == CLOCKTYPEJUSTONETIME) {
YRClockModel *model = [DataSingleton sharedInstance].dataArray[i.integerValue];
model.isOpen = NO;
[[DataSingleton sharedInstance]updateClockListDataWithRow:i.integerValue andModel:model];
dispatch_async(dispatch_get_main_queue(), ^{
[self.allClockTableView reloadData];
});
}
NSLog(@"wenwenwen");
}
}
}];
}
我来说一下写这个方法的思路
1.首先创建ViewModel这个无可厚非,在viewModel的init我们已经初始化了data。
2.所以我们接下来直接刷新tableView即可。
3.self.timeSignal 这个是干什么的呢
这句话,会创建一个时间为1秒的定时器,每隔一秒就会发出一个信号。
[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]]
我们通过RAC的 filter:这个方法来筛选出整分的时刻,也就是说,只有当时间为xx:xx:00的时候,才是我们需要处理的时间。我们筛选出来后,在通过RAC的 map:方法来将Date转换成String类型,因为我们需要的是String类型,而不是Date类型,同时我们的时间规格是(小时:分钟)。这样我们就获取了一个时间信号timeSignal,RAC真是很强大。
4.我们第一次订阅这个timeSignal,首先我们判断是否时间为00:00如果是,说明我们到了新的一天,我们需要更新今天需要响铃的Array。其次就是我们需要遍历我们的dataArray,来获取每一个model的time是否在响铃Array中,如果存在并相等就响铃,如果不存在或不等就什么都不做。
其中比较怪的一个就是仅一次的时候,这个响铃后需要将model的isOpen关闭,同时还要关闭Switch。
第二个就是(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath这个方法。
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
YRAllClockTableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (!cell) {
cell = [[YRAllClockTableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentify];
[self.viewModel getClockSimpleDataWithIndex:indexPath.row];
cell.timeLb.text = self.viewModel.clockTime;
cell.typeLb.text = self.viewModel.clockType;
cell.clockNameLb.text = self.viewModel.clockName;
cell.statusSwitch.on = self.viewModel.isOpen;
cell.timeLb.textColor = self.viewModel.timeColor;
cell.typeLb.textColor = self.viewModel.color;
cell.statusLb.textColor = self.viewModel.color;
cell.clockNameLb.textColor = self.viewModel.color;
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"HH:mm"];
cell.statusLb.text = [self backStatusTimeWithIndex:indexPath.row andNowTime:[formatter stringFromDate:[NSDate date]]];
@weakify(self);
RAC(cell.statusLb, text) = [self.timeSignal map:^id(NSString *value) {
@strongify(self);
NSString *str = [self backStatusTimeWithIndex:indexPath.row andNowTime:value];
if ([str isEqualToString:@"关闭"] && cell.statusSwitch.on) {
cell.statusSwitch.on = NO;
}
return str;
}];
self.viewModel.openSignal = [cell.switchSubject map:^id(NSNumber *value) {
return [RACTuple tupleWithObjects:value, @(indexPath.row), nil];
}];
[self.viewModel.openSignal subscribeNext:^(id x) {
@strongify(self);
cell.statusLb.text = [self backStatusTimeWithIndex:indexPath.row andNowTime:[formatter stringFromDate:[NSDate date]]];
}];
}
return cell;
}
说实话,我总感觉这个方法我写的有问题,但是有找不到问题所在。怪怪的。
来说一下这个方法的思路。
1.通过[self.viewModel getClockSimpleDataWithIndex:indexPath.row];注意这里使用的getClockSimpleDataWithIndex:方法。
2.对cell的各个view进行赋值,按照原来的应该是RAC(xxx) = RACObserve(xxx)这样来赋值,想了很久发现,不是所有场景都需要,虽然很好用。这里我们并不需要实时更新Cell的值,所以我们不需要使用RACObserve与RAC,只需要正常赋值即可,但是我们赋值依然很漂亮,原因就是我们已经在ViewModel中处理完成了。这也就是ViewModel的好处所在,减少Controller里的业务逻辑。
3.我们第二次订阅timeSignal,为什么使用呢,因为我们想要监听时间,然后让cell的StatusLb实现一个倒计时的效果。这个倒计时的处理我们放到了backStatusTimeWithIndex: andNowTime:方法中。稍后我们再看。
4.我们在改变Switch的时候,我们其实是需要修改我们的plist的,于是我们通过,cell.switchSubject来监听switch的改变。为了让数据处理在viewModel中我有在viewModel中创建了一个openSignal来监听cell.switchSubject。就是这里,我觉着写的有些过于麻烦了。
这里需要说明一下,一个signal最好不要被多次订阅,因为这样每次发出信号发给多个订阅者可能会影响程序运行。
第三个接下来说一下backStatusTimeWithIndex: andNowTime:方法
-(NSString *)backStatusTimeWithIndex:(NSInteger)index andNowTime:(NSString *)value{
[self.viewModel getClockSimpleDataWithIndex:index];
if (self.viewModel.isOpen) {
NSArray *timeArr = [value componentsSeparatedByString:@":"];
NSNumber *hour = timeArr[0];
NSNumber *minute = timeArr[1];
NSArray *modelTimeArr = [self.viewModel.clockTime componentsSeparatedByString:@":"];
NSNumber *modelHour = modelTimeArr[0];
NSNumber *modelMinute = modelTimeArr[1];
NSInteger time = (modelHour.integerValue * 60 + modelMinute.integerValue) - (hour.integerValue * 60 + minute.integerValue);
NSString *statusTime;
if (time > 0) {
statusTime = [NSString stringWithFormat:@"%ld小时%ld分钟后响铃", time / 60,time % 60];
}else if(time < 0){
statusTime = [NSString stringWithFormat:@"%ld小时%ld分钟后响铃", (time + 24 * 60) / 60,(time + 24 * 60) % 60];
}else{
if (self.viewModel.clockModelType == CLOCKTYPEJUSTONETIME) {
statusTime = @"关闭";
}else{
statusTime = @"24小时后响铃";
}
}
return statusTime;
}else{
return @"关闭";
}
}
思路:
1.通过value来获取到当前时间,通过index来获取到对应的model。
2.如果isOpen为真我们进行进一步处理,否则返回关闭。
3.如果为真:获取到当期时间与model.clockTime后,首先进行分割,因为格式都为xx:xx所以我们通过:来分割。然后都转换成分钟进行比较。
4.if modelTime - nowTiem < 0 那么我们需要在差值结果下再加2400。
5.if modelTime - nowTiem > 0 那么我们可以直接使用。
6.有个特殊的地方就是model. clockModelType == CLOCKTYPEJUSTONETIME时,我们需要将返回值改为关闭,因为仅一次的时候需要将其改为关闭。否则返回24小时后响铃。
EditViewController
首先init方法,来接收上个界面传过来viewModel与对应的index,为什么需要index,主要原因是因为我们在updateModel的时候需要知道它的index。
-(instancetype)initWithIndex:(NSInteger)clockIndex{
self = [super init];
if (self) {
self.viewModel = [[YRClockViewModel alloc]init];
[self.viewModel getClockDataWithIndex:clockIndex];
self.clockIndex = clockIndex;
}
return self;
}
这个界面我看了一下有4个比较有意思的地方,我们来看看。
第一个bindingViewModel
-(void)bindingViewModel{
[self.viewModel getClockDataWithIndex:self.clockIndex];
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"HH:mm"];
NSDate *pickerDate = [formatter dateFromString:self.viewModel.clockTime];
self.datePicker.date = pickerDate;
self.datePicker.datePickerMode = UIDatePickerModeTime;
[[self.datePicker rac_newDateChannelWithNilValue:pickerDate]subscribeNext:^(NSDate *x) {
NSDateFormatter *formatter = [[NSDateFormatter alloc]init];
[formatter setDateFormat:@"HH:mm"];
self.viewModel.clockTime = [formatter stringFromDate:x];
}];
RAC(self.ringView.contentLb, text) = RACObserve(self.viewModel, clockRing);
RAC(self.repeatView.contentLb ,text) = RACObserve(self.viewModel, clockType);
RAC(self.remindView.contentLb, text) = RACObserve(self.viewModel, clockRemindType);
RAC(self.nameView.contentLb, text) = RACObserve(self.viewModel, clockName);
RAC(self.volumeView.slider, value) = RACObserve(self.viewModel, clockVolume);
}
1.rac真的很强大,dataPicker都有对应的监听方法让我是在没想到。通过这个方法来监听,然后更改viewModel的clockTime
2.这里我们使用RAC(xx) = RACObserver(xx)来进行绑定,因为当这些值改变的时候,我们需要及时修改控件的值。
第二个地方Command方法执行
这里要说的就是上面提到的Command上面时候执行execute:方法,这里有两个一个saveCommand一个是deleteCommand
saveCommand
//save点击事件处理
-(void)saveClick:(id)sender{
[self.viewModel.saveCommand execute:@(self.clockIndex)];
[self dismissViewControllerAnimated:YES completion:nil];
}
我们在按钮的@selector方法中执行execute方法
deleteCommand
[[view rac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(id x) {
@weakify(self);
UIAlertAction *actionSure = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
@strongify(self);
[self.viewModel.deleteCommand execute:@(self.clockIndex)];
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:nil];
});
}];
这里我们使用rac_signalForControlEvents:UIControlEventTouchUpInside来监听按钮的UIControlEventTouchUpInside事件,在监听到之后在里面执行execute:方法。
第三个viewModel的clockModelType属性
看viewModel我们不难发现,我们已经有了clockType与clockRemindType为什么还需要clockModelType与clockModelRemindType,主要原因就是我们保存的是枚举类型。而我们的clockType与clockRemindType是String类型,是用来提供显示的,与存储类型不匹配,所以这里使用clockModelType与clockModelRemindType保存数据。
第四个,保存
我们的所有在EditViewController中修改的数据,我们都不需要立刻保存,只有在点击保存后这才进行保存。
第四个貌似有点鸡肋。
还有很多功能没有实现,只是简单的实现了基本功能,巩固MVVM与RAC的使用。如果有时间会继续完善后面功能。
接下来打算搞搞Python了
原因:iPhone买不起,我的mac快要退休了。
有兴趣的一起搞起来。
github地址:
https://github.com/hzj7510/YRClock.git