iOS 日程事历行程功能实现(Object-C)

1,970 阅读4分钟

前言

最近看之前写的代码突然感觉非常陌生,自我感觉实现的逻辑还不错,所以想整理一下,一是加深印象,二是想分享一下。

日程事历大家都用过,苹果也有自己的。但仔细想想里面计算视图frame的逻辑还是非常有意思的。

需求

  1. 显示用户一天内的所有行程。
  2. 行程卡片的高度由行程时长决定
  3. 行程卡片的宽度由同一时间段内重叠的行程个数决定。
  4. 行程卡片的开始时间结束时间按照半小时取整,取整规则如下
企业微信截图_16840415705466.png

设计图如下:

image.png

分析

这个需求其实最难的一点在于如何计算出同一时间段内重叠行程的数量。因为有按照半小时取整的逻辑,可以通过0-48遍历,看每个时间线上,有多少个行程是包含该时间线的。

某两个时间段内的行程重叠的数量不相同的,也就是说卡片的宽度并不是都相同的,所以第一步,肯定是将行程分成堆,然后在每一堆中计算出卡片合适的宽度。

最后一点就是,同一时间线上的卡片,要计算出合适的index,然后再通过宽度才能知道起x轴的坐标。

代码

卡片对应的ViewModel


@interface ZDCalendarEventViewModel : NSObject

@property (nonatomic, strong) NSString *eventName;

@property (nonatomic, strong) NSString *eventStatusName;

@property (nonatomic, assign) NSInteger generateType;

@property (nonatomic, strong) NSString *eventTime;
/// 开始时间的序列,通过startTimeIndex计算y坐标
@property (nonatomic, assign) NSInteger startTimeIndex;
/// 结束时间的序列,通过startTimeIndex计算y坐标
@property (nonatomic, assign) NSInteger endTimeIndex;
///时间的持续数,半小时为单位,通过continueTimeCount计算高速
@property (nonatomic, assign) NSInteger continueTimeCount;
///水平方向的序列,通过horizontalIndex 来计算x坐标
@property (nonatomic, assign) NSInteger horizontalIndex;

- (void)bindModel:(ZDScheduleEventModel *)model;

@end

每个堆的ViewModel

@interface ZDScheduleEventHeapViewModel : NSObject
/// 事件viewModelList
@property (nonatomic, copy) NSArray<ZDCalendarEventViewModel *> *eventViewModels;
/// 事件一行最多的数量
@property (nonatomic, assign) NSInteger maxLineCount;

- (void)bindEventViewModels:(NSArray *)eventList;

@end

将ZDCalendarEventViewModel分成若干堆。 eventViewModels是通过开始时间排序的。

- (void)bindViewModels:(NSArray *)eventViewModels {

    NSMutableArray *heapList = [[NSMutableArray alloc] init];
    NSInteger startIndex = 0;
    NSInteger endIndex = 0;
    NSMutableArray *temp;
    // 先分成二维数组
    for (ZDCalendarEventViewModel *model in eventViewModels) {
        // 在时间范围内
        if (model.startTimeIndex >= startIndex &&
            model.startTimeIndex < endIndex) {
        // 如果不在时间范围内,重新创建List来存放
        } else {
            temp = [NSMutableArray array];
            [heapList addObject:temp];
            startIndex = model.startTimeIndex;
        }
        // 扩充结束时间
        endIndex = MAX(model.endTimeIndex, endIndex);
        [temp addObject:model];
    }

    NSMutableArray *heapVMList = [NSMutableArray array];
    for (NSArray *heap in heapList) {
        ZDScheduleEventHeapViewModel *heapViewModel = [[ZDScheduleEventHeapViewModel alloc] init];
        [heapViewModel bindEventViewModels:heap];
        [heapVMList addObject:heapViewModel];
    }
    self.eventHeapViewModels = [heapVMList copy];
}

其中ZDScheduleEventHeapViewModel的bindEventViewModels方法如下。

- (void)bindEventViewModels:(NSArray *)eventList {

    self.eventViewModels = eventList;
    // 
    NSMutableArray *eventViewModels = [NSMutableArray arrayWithArray:eventList];
    NSInteger lineMaxCount = 0;
    // TODO:优化
    for (int i = 0; i < 48; i ++) {
        //需要在此次循环计算出x的
        NSMutableArray *layoutArr = [NSMutableArray array];
        //已经在之前循环计算过x的
        NSMutableDictionary *layoutedMap =[NSMutableDictionary dictionary];
        for (ZDCalendarEventViewModel *event in eventViewModels) {
            // 行程跨度,包含当前时间线
            if (event.startTimeIndex <= i && event.endTimeIndex > i) {
                // 没有计算过index的,放入数组内
                if (event.horizontalIndex == ZD_Schedule_Deafult_Index) {
                    [layoutArr addObject:event];
                // 在之前时间线循环中已经计算出inde的,放字典里,查index是否占用情况。
                } else {
                    [layoutedMap setObject:event forKey:@(event.horizontalIndex)];
                }
            }
        }
        //已经被占用个数,需要跳过
        NSInteger occupationCount = 0;
        for (int j = 0; j < layoutArr.count; j ++) {
            ZDCalendarEventViewModel *event = layoutArr[j];
            // 找到第1个没有被占用的index
            while ([layoutedMap objectForKey:@(j + occupationCount)] != nil) {
                occupationCount ++;
            }
            event.horizontalIndex = j + occupationCount;
        }
        // 数量 = 本次计算index的 + 之前计算过index的
        NSInteger allCount = layoutArr.count + layoutedMap.count;
        if (allCount > lineMaxCount) {lineMaxCount = allCount;}
    }
    // 找出该堆中数量最大的
    self.maxLineCount = lineMaxCount;
}

按照时间线遍历,在该时间线上的行程分为2种,一种是从之前的时间线延续下来的,这种的行程在x轴上的index已经固定了,另外一种是起始在该时间线上的,这种行程在x轴上的index需要看之前的index有没有被占用,如果占用需要向后移动。

优化

可以看到上面的方法内是通过0-48来遍历的,其实不用,可以在上一步中的遍历中(分堆过程),记录出堆的ViewModel的开始时间和结束时间,通过开始时间和结束时间来代替0-48来遍历,可以优化一部分性能。

成品展示

数据:

tripList = (
			{
				oneLevelCode = "DBC2022019";
				bizExecutionTime = "2023-05-30 10:30:00";
				endTime = "11:30";
				bizExecutionEndTime = "2023-05-30 11:30:00";
				isTimeOut = 0;
				state = "dcl";
				twoLevelCode = "DBC2022019002";
				begTime = "10:30";
				oneLevelName = "自定义";
				who = "";
				generateType = 2;
				twoLevelName = "休息";
			},
			{
				bizExecutionTime = "2023-05-30 11:30:00";
				endTime = "12:30";
				bizExecutionEndTime = "2023-05-30 12:30:00";
				isTimeOut = 0;
				state = "dcl";
				twoLevelCode = "DBC2022019002";
				oneLevelName = "自定义";
				begTime = "11:30";
				who = "";
				generateType = 2;
				twoLevelName = "休息";
			},
			{
				bizExecutionTime = "2023-05-30 12:00:00";
				endTime = "12:30";
				bizExecutionEndTime = "2023-05-30 12:30:00";
				isTimeOut = 0;
				state = "dcl";
				twoLevelCode = "DBC2022019002";
				oneLevelName = "自定义";
				begTime = "12:00";
				who = "";
				generateType = 2;
				twoLevelName = "休息";
			},
			{
				bizExecutionTime = "2023-05-30 12:30:00";
				endTime = "14:00";
				bizExecutionEndTime = "2023-05-30 14:00:00";
				isTimeOut = 0;
				state = "dcl";
				twoLevelCode = "DBC2022019002";
				oneLevelName = "自定义";
				begTime = "12:30";
				who = "";
				generateType = 2;
				twoLevelName = "休息";
			},
			{
				bizExecutionTime = "2023-05-30 12:30:00";
				endTime = "17:00";
				bizExecutionEndTime = "2023-05-30 17:00:00";
				isTimeOut = 0;
				state = "dcl";
				twoLevelCode = "DBC2022019002";
				begTime = "12:30";
				oneLevelName = "自定义";
				who = "";
				generateType = 2;
				twoLevelName = "休息";
			},
			{
				bizExecutionTime = "2023-05-30 13:30:00";
				endTime = "14:00";
				bizExecutionEndTime = "2023-05-30 14:00:00";
				isTimeOut = 0;
				state = "dcl";
				twoLevelCode = "DBC2022019002";
				begTime = "13:30";
				oneLevelName = "自定义";
				who = "";
				generateType = 2;
				twoLevelName = "休息";
			},
			{
				bizExecutionTime = "2023-05-30 14:00:00";
				endTime = "14:30";
				bizExecutionEndTime = "2023-05-30 14:30:00";
				isTimeOut = 0;
				state = "dcl";
				twoLevelCode = "DBC2022019002";
				oneLevelName = "自定义";
				begTime = "14:00";
				who = "";
				generateType = 2;
				twoLevelName = "休息";
			}
		); 

UI展示如下:

wecom-temp-128593-761045a0194b1b7ee90656f396f90b30.jpg