专题:临时区iOS Developer

RAC&MVVM来吧,搞个小闹钟(iOS)

2017-04-15  本文已影响324人  Rokkia

写在文章之前

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

Paste_Image.png

安卓大概是这样。

Paste_Image.png

不要问我为什么要看朋友手机,其实,我就是那个做iOS开发没有iPhone的开发人员。

言归正传,没有就搞搞呗。

框架依旧是MVVM,正好来巩固一下。

开始之前的思考

我如何保存闹钟的数据,比如名字,闹钟时间,重复类型等,放到哪里呢?网络接口肯定用不到,那就本地存储呗,数据并不多,简单的来个plist吧,用过之后我才发现这是有多坑。这是后话,所以说,开始前的思考还是很重要的。

开始吧

先看一下目录结构。

Paste_Image.png

View

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

Paste_Image.png

这里我们说一个使用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];

看一下四句话的效果。


RAC_switch录制.gif

数据处理

既然使用的是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

上一篇 下一篇

猜你喜欢

热点阅读