关于首页倒计时处理一些细节

下面是效果图

这个模块是展示促销商品模块的:

需求有下面的几点:

  • 上面是频道栏目 可以左右滑动进行切换
  • 下面是促销商品的列表
  • 商品栏目数目为2 为了后期兼容做成可以左右滚动
  • 当两个其中只有一个已经停止就把停止的商品显示 DEAL ENDED
  • 当两个都已经停止就去除对应的栏目

本来想把定时器做到 Cell 里面或者上面显示时间的控件里面 开始做的时候没发现什么问题。后来切换了频道,发现其他频道已经两个结束了竟然没有删除对应的栏目

经过思考,导致这个问题出现的原因是

促销商品展示的 Cell 是重用的,开始的时候其他栏目是没有赋值的。导致是不能收到已经停止的消息的,自然也就没办法从列表里面进行移除

解决的方案就是对数据源进行各自的监听,当数据源显示时间已经停止的时候,就移除对应的数据源,重新刷新界面。

新建一个基类用于管理倒计时GBSaleTimeModel

因为倒计时只分为还没有开始 正在进行 已经结束三种时间段,我们新建一个 ENUM 类型来标识

/**
  销售的三种状态

 - GBSaleTimeTypeNoStart: 还没有开始
 - GBSaleTimeTypeStarting: 开始销售中
 - GBSaleTimeTypeEnded: 已经结束
 */
typedef NS_ENUM(NSUInteger, GBSaleTimeType) {
    GBSaleTimeTypeNoStart,
    GBSaleTimeTypeStarting,
    GBSaleTimeTypeEnded
};

新建一个 typedef用户 Block 进行回调

/**
 当定时器值改变的时候就回调

 @param model GBSaleTimeModel
 */
typedef void(^GBSaleTimeValueChangeCompletionHandle)(GBSaleTimeModel *model);

让外部可以知道当前的数据源处于什么状态,我们新建一个只读的变量用于让外接知道当前数据源处于什么状态。

/**
 当前销售状态
 */
@property (nonatomic, assign, readonly) GBSaleTimeType saleTimeType;

我们是只读的属性 我们实现一个 Get 方法

- (GBSaleTimeType)saleTimeType {
    NSTimeInterval nowTimeInterval = [[NSDate date] timeIntervalSince1970];
    if (nowTimeInterval < self.startTimeInterval) {
        return GBSaleTimeTypeNoStart;
    } else if (nowTimeInterval < self.endTimeInterval) {
        return GBSaleTimeTypeStarting;
    } else {
        return GBSaleTimeTypeEnded;
    }
}

因为是获取距离现在时间的状态,自然要获取的是现在时间距离1970的时间戳,比较时间戳。

如果现在时间戳小于开始的时间戳 标识还没有开始销售

如果现在的时间戳大于等于开始时间戳并且小于结束的时间戳 标识正在销售

如果现在的时间戳大于等于结束的时间戳 标识已经结束销售

因为开始的时间和结束的时间是后段给的 我们无法知道所有新建两个属性 只读让子类实现 get 方法

/**
 开始时间 子类重写 get 方法
 */
@property (nonatomic, assign, readonly) NSTimeInterval startTimeInterval;

/**
 结束时间 子类重写 get 方法
 */
@property (nonatomic, assign, readonly) NSTimeInterval endTimeInterval;

新建一个只读的属性 让外部可以知道现在时间距离开始 或者 结束 时间段 方便做倒计时

/**
 时间段 如果还没有开始就是现在时间和开始时间的间距 如果是已经开始就等于现在时间和结束时间的间距 如果是已经结束就等于0
 */
@property (nonatomic, assign, readonly) NSTimeInterval nowTimePeriod;
- (NSTimeInterval)nowTimePeriod {
    NSTimeInterval nowTimeInterval = [[NSDate date] timeIntervalSince1970];
    if (self.saleTimeType == GBSaleTimeTypeNoStart) {
        return self.startTimeInterval - nowTimeInterval;
    } else if (self.saleTimeType == GBSaleTimeTypeStarting) {
        return self.endTimeInterval - nowTimeInterval;
    } else {
        return 0;
    }
}

我们判断目前的状态 如果是还没有开始就 获取距离开始的时间

如果是正在销售 就获取距离结束的时间

如果是已经销售结束 就赋值等于0

我们新增一个注册监听的方法 让外界监听销售状态和改变倒计时状态

/**
 注册监听时间改变的回调

 @param completionHandle 回调
 */
- (void)registerSaleTimeValueChangedCompletionHandle:(GBSaleTimeValueChangeCompletionHandle)completionHandle;
- (void)registerSaleTimeValueChangedCompletionHandle:(GBSaleTimeValueChangeCompletionHandle)completionHandle {
    if (!_completionHandleList) {
        _completionHandleList = [NSMutableArray array];
    }
    if (completionHandle) {
        [_completionHandleList addObject:completionHandle];
    }
    [self valueChnaged];
    if (!_saleTimer && self.endTimeInterval > self.startTimeInterval && self.startTimeInterval > 0) {
        _saleTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(valueChnaged) userInfo:nil repeats:YES];
    }
}

为什么要把 Block 添加到数组里面 ?

因为要做一个功能 就是让多个人进行监听同一个对象的回调 这个也直接导致下面的一个问题的出现

为什么要判断 Block 存在再添加呢?

因为如果外部调用方法不实现 block 就会直接的崩溃

为什么要在定时器之前还调用一下valueChnaged值改变的方法呢?

因为可能用户注册的时候 倒计时已经停止 或者 不满足定时器开启的条件 外接就无法得到对应的状态 会出现一些问题无法修复

- (void)valueChnaged {
    for (int i = 0; i < _completionHandleList.count; i ++) {
        GBSaleTimeValueChangeCompletionHandle completionHandle = _completionHandleList[i];
        completionHandle(self);
    }
    if (self.saleTimeType == GBSaleTimeTypeEnded) {
        [_saleTimer invalidate];
        [_completionHandleList removeAllObjects];
    }
}

因为要监听还没有显示的模块信息 所以我们在给整个模块赋值的时候 进行遍历销售的商品进行监听倒计时

如果其中的一组已经全部停止 就删除对应的频道

- (void)checkSaleItem:(BOOL)needRegister {
    NSMutableArray *nowSaleModels = [NSMutableArray array];
    @weakify(self);
    [_headerModel.homeSaleModels enumerateObjectsUsingBlock:^(GBHomeSaleModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        @strongify(self);
        NSMutableArray *hotSaleGoodList = [NSMutableArray array];
        [obj.hotSaleGoodList enumerateObjectsUsingBlock:^(GBHomeSaleItemModel * _Nonnull obj1, NSUInteger idx1, BOOL * _Nonnull stop) {
            @strongify(self);
            if (obj1.saleTimeType != GBSaleTimeTypeEnded) {
                // 如果还没有结束才显示
                [hotSaleGoodList addObject:obj1];
                if (needRegister) {
                    [obj1 registerSaleTimeValueChangedCompletionHandle:^(GBSaleTimeModel *model) {
                        @strongify(self);
                        if (model.saleTimeType == GBSaleTimeTypeEnded) {
                            [self checkSaleItem:NO];
                            [self registerTableViewGroup];
                            [self.tableView reloadData];
                        }
                    }];
                }
            }
        }];
        if (hotSaleGoodList.count > 0) {
            [nowSaleModels addObject:obj];
        }
    }];
    _headerModel.homeSaleModels = nowSaleModels;
}

为什么要在方法添加一个是否需要注册的参数呢?

因为 我们在倒计时结束的时候 重新走了一次本方法 进行数据的筛选。 如果我们每次都注册 导致如果结束的时候回调就会死循环 如果在添加之前判断时候结束 也是可以的

为什么要做销售商品数组大于零 就添加对应的频道?

因为之前做的是 如果商品已经销售停止 就删除对应的元素 让界面只显示正在销售的

后来产品说只显示一个元素界面会不好看 就改成了如果两个都销售停止才删除对应的频道 如果只有一个就让已经销售停止的展示 DEAL ENDED

我们在销售商品的试图 Cell 上面赋值数据源的地方进行监听倒计时

    [itemModel registerSaleTimeValueChangedCompletionHandle:^(GBSaleTimeModel *model) {
        @strongify(self);
        if (self.itemModel != model) {
            return ;
        }
        self.saleTimeLabel.timeInterval = model.nowTimePeriod;
        NSString *startLabelText;
        if (model.saleTimeType == GBSaleTimeTypeNoStart) {
            startLabelText = @"Starts in";

        } else {
            startLabelText = @"Ended in";
        }
        BOOL isHideDealEnded = model.saleTimeType == GBSaleTimeTypeEnded;
        UIColor *startTimeColor = isHideDealEnded ? GB_COLOR_RGB(153, 153, 153, 1.0) : GB_COLOR_RGB(51, 51, 51, 1.0);
        self.startTimeLabel.textColor = startTimeColor;
        self.dealEndedView.hidden = !isHideDealEnded;
        self.startTimeLabel.text = startLabelText;
    }];

这个方法存在 一个严重的问题就是 当切换频道 就会cell 重用 新的数据源就会重新的注册 block

但是之前注册的 block 还是存在在内存里面就会出现显示的倒计时的时间不正确

        if (self.itemModel != model) {
            return ;
        }

这一句代码就是为了判断如果回调的 block 如果不是现在显示的数据源回调就不会往下继续走

其实还有一个更好的方法 就是注册的时候保留 block 对象 cell 进行刷新的时候 移除之前注册的

这样不仅解决了数据源显示错误问题 而且可以让之前注册 block 不会回调 减少性能消耗