背景
我们 App 非常重视时间的显示规则,比如在首页显示的新闻如果是一个礼拜之前的,那就应该隐藏时间,避免让用户感觉该新闻非常陈旧,但是其他场景比如用户的评论,就不会在乎时效性了。
之前新闻App针对不同的时间策略,写了不同的函数来处理显示逻辑,这些函数内容雷同冗长,又不方便复用代码,每次新增一个时间显示策略,又得copy一份代码,然后一顿修改。
没有任何扩展性可言。
现在我们提供了一个时间显示的工具类 XXCustomDateFormatter(前缀保密),它可以在远程动态配置时间显示策略,如果是修改某个场景的策略,客户端无需修改代码,如果是新增场景,客户端仅仅需要新增一个 type 即可。
用法
目前已有的时间显示策略如下:
typedef NS_ENUM(NSInteger, CustomDateFormatterType) {
kCustomDateFormatterTypeDefault = 0, // 默认场景
kCustomDateFormatterTypeTimeLine, // 主TimeLine
...........
};
如果你对应的业务使用的是默认策略,那么如下一行代码即可获取你想要的string
NSString *timeMsg = [XXCustomDateFormatter customStringWithTimeInterval:listItem.timeStamp type:kCustomDateFormatterTypeDefault];
原理
我们在远程可以定义多个字典,每个字典对应一个特殊的场景,每个场景的规则都不一样,key的前缀目前包含 T(秒) D(日) Y(年) MAX(最大值),value 对应的就是显示给用户看的时间,比如value = “yyyy年MM月dd日” 代表我们给用户呈现的效果是年月日。
估计目前为止读者还是一脸懵逼,这件事的确不太好理解。接下来我举一个例子。
时间策略实例:
// 默认时间显示策略
static NSString *kQNDefaultTimeDisplayConfig = @"{\"T-60\": \"刚刚\",\
\"T-3600\": \"%m分钟前\",\
\"D-0\": \"%h小时前\",\
\"D-1\": \"昨天HH:mm\",\
\"D-2\": \"前天HH:mm\",\
\"Y-0\": \"MM月dd日HH:mm\",\
\"MAX-0\": \"yyyy年MM月dd日\",\
\"keyOrder\": \"T-60|T-3600|D-0|D-1|D-2|Y-0|MAX-0\"}";
这是我们配置的一个字典,该字典用于默认场景,我们客户端如何解析这个字典呢?
现在我们假设有一个服务器下发的时间戳叫 listItem. timeStamp,它代表一篇文章的发布时间,listItem. timeStamp 与当前的时间差叫做 time.
首先,我们根据 keyOrder 来决定顺序,首先来到 T-60, 我们把time和60做比较,如果小于60客户端就显示这篇文章的发布时间为 “刚刚”
如果time < 3600,我们就显示这篇文章的发布时间为 “%m分钟前”,当然这个%m需要我们自己计算出来并替换成真实的时间。
接下来就好理解了, D-0 代表如果文章是今日之内发布的,我就显示成 "%h小时前" ,这个 h当然也要客户端计算,同理 D-1 代表昨天发布的,Y-0代表今年发布,Y-1代表去年发布的。
源码
h文件
#import <Foundation/Foundation.h>
/**
根据CommonValues动态地格式化时间
更具有灵活性
下发时间配置示例:
"time_line_time_display_config": {
"MAX-259200": "",
"T-60": "刚刚",
"T-3600": "%m分钟前",
"T-28800": "%h小时前",
"D-0": "8小时前",
"D-1": "昨天HH:mm",
"D-2": "前天HH:mm",
"keyOrder": "MAX-259200|T-60|T-3600|T-28800|D-0|D-1|D-2"
}
具体规则如下见 http://tapd.oa.com/newsapp_android/markdown_wikis/view/#1010056241007851563
*/
typedef NS_ENUM(NSInteger, CustomDateFormatterType) {
kCustomDateFormatterTypeDefault = 0, // 默认场景
kCustomDateFormatterTypeSimple, // 简单版本时间显示(比如用于用户评论)
};
@interface XXCustomDateFormatter : NSObject
/**
根据CommonValues下发配置动态地格式化时间
@param timeInterval 时间
@param type 时间显示场景
@return 格式化后的时间
*/
+ (NSString *)customStringWithTimeInterval:(NSTimeInterval)timeInterval type:(CustomDateFormatterType)type;
/**
保存一份commonValues到QNCustomDateFormatter
(因为它也用于24 Hour widget Extension这个target,不适合引用CRemoteConfig)
@param dic 最新的commonValues
*/
+ (void)updateCommonValueWithDic:(NSDictionary *)dic;
@end
m文件
#import "XXCustomDateFormatter.h"
static NSDictionary *commonValues;
// key 匹配需要的pattern
static NSString *kQNKeyPatternMax = @"MAX-"; // 后面携带的是秒单位的时间点,大于当前时间点,则匹配不显示
static NSString *kQNKeyPatternTime = @"T-"; // 后面携带的是秒单位的时间点,小于当前时间点则匹配
static NSString *kQNKeyPatternDay = @"D-"; // 后面携带的是和今天差的天数,0:今天;1:昨天;以此类推
static NSString *kQNKeyPatternYear = @"Y-"; // 后面写带的是和当年差的年数,0:当年,1:去年;以此类推
static NSString *kQNKeyPatternAbsolute = @"ABS-"; // 绝对日期,后面匹配的是x.x.x,代表的年月日,x的情况下代表任意 ABS-x.1.1 某年第一天
// value 匹配需要的pattern
static NSString *kQNValuePatternSecond = @"%s"; // 把时间差按照秒替换
static NSString *kQNValuePatternMinute = @"%m"; // 把时间差按照分钟替换
static NSString *kQNValuePatternHour = @"%h"; // 把时间差按照小时替换
static NSString *kQNValuePatternDay = @"%d"; // 把时间差按照天替换
static NSString *kQNValuePatternWeek = @"%w"; // 把时间差按照周替换
// 简单版本时间(比如用于用户评论)显示策略
static NSString *kQNSimpleTimeDisplayConfig = @"{\"T-60\": \"刚刚\",\
\"T-3600\": \"%m分钟前\",\
\"D-0\": \"%h小时前\",\
\"D-1\": \"昨天\",\
\"D-2\": \"前天\",\
\"MAX-0\": \"MM月dd日\",\
\"keyOrder\": \"T-60|T-3600|D-0|D-1|D-2|MAX-0\"}";
// 默认时间显示策略
static NSString *kQNDefaultTimeDisplayConfig = @"{\"T-60\": \"刚刚\",\
\"T-3600\": \"%m分钟前\",\
\"D-0\": \"%h小时前\",\
\"D-1\": \"昨天HH:mm\",\
\"D-2\": \"前天HH:mm\",\
\"Y-0\": \"MM月dd日HH:mm\",\
\"MAX-0\": \"yyyy年MM月dd日\",\
\"keyOrder\": \"T-60|T-3600|D-0|D-1|D-2|Y-0|MAX-0\"}";
@implementation XXCustomDateFormatter
+ (NSString *)customStringWithTimeInterval:(NSTimeInterval)timeInterval type:(CustomDateFormatterType)type {
NSDictionary *dic = [self _getConfigDicWithType:type];
// 获取失败或者defaut情况下使用默认策略:DEFAULT_TIME_DISPLAY_DEFAULT
if (!CHECK_VALID_DICTIONARY(dic) || !CHECK_VALID_STRING(dic[@"keyOrder"])) {
dic = [NSJSONSerialization JSONObjectWithData:[kQNDefaultTimeDisplayConfig dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
if (!CHECK_VALID_DICTIONARY(dic) || !CHECK_VALID_STRING(dic[@"keyOrder"])) {
QN_ASSERT(NO, @"NSJSONSerialization JSONObjectWithData Error");
return @"";
}
}
NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
time_t pubdate = [date timeIntervalSince1970];
struct tm *pubDate = localtime((const time_t *)&pubdate);
// 时间戳异常
if (pubDate == NULL) {
QN_E(@"zhiyun invalid date, timeInterval = %@", @(timeInterval));
return @"";
}
int yearOfPubDate = pubDate->tm_year + 1900;
int monOfPubDate = pubDate->tm_mon + 1;
int dayOfPubDate = pubDate->tm_mday;
int hourOfPubDate = pubDate->tm_hour;
int minOfPubDate = pubDate->tm_min;
time_t now = time(0);
struct tm *today = localtime((const time_t *)&now);
int yearOfToday = today->tm_year + 1900;
// 现在与发布时间的时间差
NSInteger times = labs(now - pubdate);
NSString *keyOrder = dic[@"keyOrder"];
__block NSString *resultStringDate = @"";
NSArray<NSString *> *keyArray = [keyOrder componentsSeparatedByString:@"|"];
[keyArray enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSRange range = [obj rangeOfString:@"-"];
NSUInteger index = range.location;
NSString *data = [obj substringFromIndex:(index + 1)];
NSInteger numberInKey = data.integerValue;
NSString *value = dic[obj];
if ([obj containsString:kQNKeyPatternMax]) { // MAX-
if (times > numberInKey) {
resultStringDate = [QNCustomDateFormatter formatDateWithConfigValue:value
minOfPubDate:minOfPubDate
hourOfPubDate:hourOfPubDate
dayOfPubDate:dayOfPubDate
monOfPubDate:monOfPubDate
yearOfPubDate:yearOfPubDate
times:times];
*stop = YES;
}
} else if ([obj containsString:kQNKeyPatternTime]) { // T-
if (times < numberInKey) {
resultStringDate = value;
if ([value containsString:kQNValuePatternSecond]) { // 秒
resultStringDate = [value stringByReplacingOccurrencesOfString:kQNValuePatternSecond withString:[NSString stringWithFormat:@"%zd", times]];
} else if ([value containsString:kQNValuePatternMinute]) { // 分钟
resultStringDate = [value stringByReplacingOccurrencesOfString:kQNValuePatternMinute withString:[NSString stringWithFormat:@"%zd", (times / 60)]];
} else if ([value containsString:kQNValuePatternHour]) { // 小时
resultStringDate = [value stringByReplacingOccurrencesOfString:kQNValuePatternHour withString:[NSString stringWithFormat:@"%zd", (times / 3600)]];
}
*stop = YES;
}
} else if ([obj containsString:kQNKeyPatternDay]) { // D-
NSCalendar *calendar = [NSCalendar currentCalendar];
// 自己构造的时间,代表发布时间当天0点
NSDate *pubDate = [calendar dateBySettingHour:0 minute:0 second:0 ofDate:date options:0];
NSDate *nowDate = [NSDate date];
nowDate = [calendar dateBySettingHour:0 minute:0 second:0 ofDate:nowDate options:0];
NSTimeInterval interval = [nowDate timeIntervalSinceDate:pubDate];
interval = fabs(interval / 3600);
if (interval < 49) { // 今天、昨天或前天 interval == 0 || interval == 24 || interval == 48
// 先把key拼接出来
NSString *customKey = [NSString stringWithFormat:@"%@%d", kQNKeyPatternDay, (int)(interval / 24)];
resultStringDate = [QNCustomDateFormatter formatDateWithConfigValue:dic[customKey]
minOfPubDate:minOfPubDate
hourOfPubDate:hourOfPubDate
dayOfPubDate:dayOfPubDate
monOfPubDate:monOfPubDate
yearOfPubDate:yearOfPubDate
times:times];
// 避免特殊情况出现bug,比如只下发了D-0,没有下发D-1,而恰好时间是昨天,就无法从字典中就获取到D-1对应的value
if (![resultStringDate isEqualToString:@""])
*stop = YES;
}
} else if ([obj containsString:kQNKeyPatternYear]) { // Y-
if (yearOfPubDate == yearOfToday) {
resultStringDate = [QNCustomDateFormatter formatDateWithConfigValue:value
minOfPubDate:minOfPubDate
hourOfPubDate:hourOfPubDate
dayOfPubDate:dayOfPubDate
monOfPubDate:monOfPubDate
yearOfPubDate:yearOfPubDate
times:times];
*stop = YES;
}
}
}];
return resultStringDate;
}
/**
把value中的时间占位符替换成真正的时间,比如把yyyy替换成 2019
@param configValue 带时间占位符的原始字符串
@param minOfPubDate minOfPubDate description
@param hourOfPubDate hourOfPubDate description
@param dayOfPubDate dayOfPubDate description
@param monOfPubDate monOfPubDate description
@param yearOfPubDate yearOfPubDate description
@param times times description
@return 包含真正时间的结果
*/
+ (NSString *)formatDateWithConfigValue:(NSString *)configValue minOfPubDate:(int)minOfPubDate hourOfPubDate:(int)hourOfPubDate dayOfPubDate:(int)dayOfPubDate monOfPubDate:(int)monOfPubDate yearOfPubDate:(int)yearOfPubDate times:(NSInteger)times{
if (!CHECK_VALID_STRING(configValue)) {
return @"";
}
NSMutableString *result = [NSMutableString stringWithString:configValue];
if ([result containsString:kQNValuePatternHour]) { // 几小时前
[result replaceOccurrencesOfString:kQNValuePatternHour withString:[NSString stringWithFormat:@"%zd", (times / 3600)] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
return result;
}
[result replaceOccurrencesOfString:@"mm" withString:[NSString stringWithFormat:@"%02d", minOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
[result replaceOccurrencesOfString:@"HH" withString:[NSString stringWithFormat:@"%02d", hourOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
[result replaceOccurrencesOfString:@"dd" withString:[NSString stringWithFormat:@"%02d", dayOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
[result replaceOccurrencesOfString:@"MM" withString:[NSString stringWithFormat:@"%02d", monOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
[result replaceOccurrencesOfString:@"yyyy" withString:[NSString stringWithFormat:@"%02d", yearOfPubDate] options:NSLiteralSearch range:NSMakeRange(0, result.length)];
return result;
}
+ (void)updateCommonValueWithDic:(NSDictionary *)dic {
commonValues = dic;
}
/**
根据key值从CommonValue 中获取时间配置
@param key 某个显示时间的场景对应的key值
@return 时间配置字典
*/
+ (NSDictionary *)p_dictionaryValueInCommonValuesWithKey:(NSString *)key {
QN_ASSERT(CHECK_VALID_STRING(key), @"key is invalid.");
NSDictionary *result = nil;
if (CHECK_VALID_DICTIONARY(commonValues)) {
result = QNDictionary(commonValues[key], nil);
}
return result;
}
/**
根据场景获取时间配置字典
@param type 场景
@return 时间配置字典
*/
+ (NSDictionary *)_getConfigDicWithType:(CustomDateFormatterType)type {
NSDictionary *dic = [NSDictionary dictionary];
switch (type) {
case kCustomDateFormatterTypeTimeLine:
dic = [self p_dictionaryValueInCommonValuesWithKey:@"time_line_time_display_config"];
break;
case kCustomDateFormatterTypeSimple:
dic = [NSJSONSerialization JSONObjectWithData:[kQNSimpleTimeDisplayConfig dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil];
break;
default: // 使用默认策略:DEFAULT_TIME_DISPLAY_DEFAULT
break;
}
return dic;
}
@end
网友评论