我们每个人的手机都安装有微信App,肯定大家都使用过以下场景在聊天群中,当有新的成员进入的时候,可以看到 某某邀请了某某1进入群聊,或者当你是群主的时候,某管理人员邀请了几个人进入时候看到 某某管理员邀请了张三、李四,王五,张三(同名)进入了群聊。对于邀请人员或者被邀请人员,我们发现人员的名称是可以点击的,即使同名的人,点击其名字也可以跳转到各自的主页详情中。

如何存储人员的id?

一条群成员变更通知就是一串文本,我们点击的时候肯定要存储好他的人员id,然后点击的时候就能拿到对应人员的id。从最简单的存储方案来看,这条内容可以写成以下格式: 管理员刘某某邀请了张三李四王五张三进入了群聊。换成代码的格式即为:“管理员\”刘某某::1000\”邀请了\”张三::1001、李四::1002、王五::1003、张三::1004\”进入了群聊”。其中的代码格式为userName::userId这样一种形式进行拼接,中间的符号可以采用其他特殊符号代替。这里为了区别用户特殊性输入用了双冒号。多个成员之间用顿号隔开,人员与普通内容之间用符号双引号\”进行分割(斜杆为转义符)。 我们需要对这一串文字表达式进行解析进行UI显示,把人员名字后的拼接符号用户id进行过滤掉,组合成不带用户id的易懂文字内容给到用户。

解析带用户id的字符串规则,拼装成正常文本

判断待解析的目标内容是否包含我们定义的特殊连接符号与双引号,符合条件的情况下第一步以双引号将文本进行分割。分为以下步骤。

  1. 双引号将文本进行分割成不同段的数组。
  2. 遍历数组,判断不同段中是否还有顿号,如果有则需要再根据顿号进行分割成最小粒化段(userName::userId),否则直接判断是否最小粒化段(判断是否包含特殊符::),如果包含,则以特舒符再将文本分割、否则直接添加到可变字符串中进行拼接。这样我们就能够拿到userName 以及 userId. 另外准备一个数组,存储每一个被分割的最小粒化段 {userName::userId}。
  3. 记得把人员之间的顿号,以及普通文本与人员之间的双引号加回去。最终的到了正常显示的文本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
if([msgBody containsString:@"\""] && [msgBody containsString:@"::"]){
NSArray *descArr = [msgBody componentsSeparatedByString:@"\""];
NSMutableString *afterFilterIdStr = [[NSMutableString alloc] init]; //过滤出Id后要显示在界面的str
NSMutableArray *filterEmpDicArr = [[NSMutableArray alloc] init]; //过滤出成员

NSInteger findIndex = 0;
for (NSString *eachStr in descArr) {
findIndex ++;
//加入双引号
if(eachStr.length > 0){
if([eachStr containsString:@"、"]){
NSArray *namesArr = [eachStr componentsSeparatedByString:@"、"];
NSInteger otherFindIndex = 0;
for(NSString *otherName in namesArr){
otherFindIndex ++;
if([otherName containsString:@"::"]){
//需分割取得名字
NSArray *itemStrArr = [otherName componentsSeparatedByString:@"::"];
if(itemStrArr.count){
NSString *empName = itemStrArr.firstObject;
NSString *empId = itemStrArr.lastObject;
[afterFilterIdStr appendString:empName];
[filterEmpDicArr addObject:@{@"name":empName,@"id":empId}];
}
}
if(otherFindIndex < namesArr.count){
[afterFilterIdStr appendString:@"、"];
}
}

}else{
if([eachStr containsString:@"::"]){
//需分割取得名字
NSArray *itemStrArr = [eachStr componentsSeparatedByString:@"::"];
if(itemStrArr.count){
NSString *empName = itemStrArr.firstObject;
NSString *empId = itemStrArr.lastObject;
[afterFilterIdStr appendString:empName];

NSString * oldContainsStr = @"将"; //兼容移除群聊的显示结果
if([empId containsString:oldContainsStr]){
[afterFilterIdStr appendString:oldContainsStr];
NSArray *oldArr = [empId componentsSeparatedByString:oldContainsStr];
empId = oldArr.firstObject;
}

[filterEmpDicArr addObject:@{@"name":empName,@"id":empId}];
}
}else{
//普通字符串
[afterFilterIdStr appendString:eachStr];
}
}
}
if(findIndex < descArr.count){
[afterFilterIdStr appendString:@"\""];
}
}
NSString *realMsgBody = [afterFilterIdStr copy];
}

通过以上代码,我们得到realMsgBody就是需要在文本上直接显示的内容,另外我们还得到了filterEmpDicArr包含了成员id的一个map。

渲染人员名字、增加样式蓝色,表示可以点击

由上一步,我们得到可正常显示在UI上的文字,另外还得到一个人名id的字典数组。渲染显示我们采用YYLabel设置Attribute的方式。先设置全内容属性mAttributedString。然后循环遍历filterEmpDicArr,根据人名创建名字高亮的lightAttribute。根据人名得到nameRange, 最后再采用替换方式将对应range进行替换掉属性。代码:

1
[mAttributedString replaceCharactersInRange:nameRange withAttributedString:lightNameAttributeStr];

这里有一个问题,当匹配到第四个人时候,取到的nameRange是第一个人张三,结果并不对。来看看我们取nameRange的方式:
1
NSRange range = [realMsgBody rangeOfString:empName]; //可能存在多个

这个时候就需要判断名字是否在文本中多次出现。当同名时候,我们需要把匹配到该人名的range全部找出来,而不是一个range。 当遍历当前的name时候,检查该人名出现的次数,出现第几次则这个当前遍历的人名的range则是对应第几个range. 这个逻辑才是正确的!这个时候,我们可以把当前遍历的userId对应的正确的range记录下来存到一个map中。最后当用户点击蓝色某名字时候label点击处所对应的range会返回,这个时候我们就可以根据range来命中userId了。当点击同名的时候不知道点击的是谁的问题就可以迎刃而解!此处应该有点赞,哈。过奖了,只是一个技巧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
NSString *realMsgBody = [afterFilterIdStr copy];
NSMutableAttributedString *mAttributedString = [[NSMutableAttributedString alloc] initWithString:realMsgBody];
mAttributedString.yy_font = labelFont;
mAttributedString.yy_color = message_centerTextColor;
mAttributedString.yy_alignment = NSTextAlignmentCenter;
NSMutableDictionary *recordNameCountDic = [[NSMutableDictionary alloc] init]; //缓存每个名字记录的次数
NSMutableDictionary *recordRangeEmpIdDic = [[NSMutableDictionary alloc] init]; //记录每个名字的range对应的id
//循环找出名字,并且替换attribute;
for (NSDictionary *empDic in filterEmpDicArr) {
NSString *empName = empDic[@"name"];
NSString *empId = empDic[@"id"];
NSMutableAttributedString *lightNameAttributeStr = [[NSMutableAttributedString alloc] initWithString:empName];
lightNameAttributeStr.yy_font = labelFont;
lightNameAttributeStr.yy_underlineStyle = NSUnderlineStyleNone; //NSUnderlineStyleSingle;
lightNameAttributeStr.yy_color = message_centerTextColor;
lightNameAttributeStr.yy_lineSpacing = 2;

__weak typeof(self) weakSelf = self;
[lightNameAttributeStr yy_setTextHighlightRange:lightNameAttributeStr.yy_rangeOfAll
color:[UIColor colorWithRed:110/255.0f green:121/255.0f blue:151/255.0f alpha:1]
backgroundColor:[UIColor colorWithWhite:0.000 alpha:0.0]
tapAction:^(UIView *containerView, NSAttributedString *text, NSRange range, CGRect rect){
//点击名字跳转
// NSString *empName = [text.string substringWithRange:range];
NSString *empId = [recordRangeEmpIdDic objectForKey:[NSValue valueWithRange:range]];
if(empId && weakSelf.tappedNameBlock){
weakSelf.tappedNameBlock(empId);
}
}];

NSRange range = [realMsgBody rangeOfString:empName]; //可能存在多个
//同名可匹配出多个range
NSArray *rangeArr = [self rangeOfSubString:empName inString:realMsgBody];
if(rangeArr.count && rangeArr.count > 1){

//有多个重名,拿字典记录的值{name:count} 名字次数. count有存切大于1时,本次的range为 rangeArr【count】
if([recordNameCountDic objectForKey:empName]){
NSInteger nameCount = [[recordNameCountDic objectForKey:empName] integerValue];
nameCount++; //rangeArr[name]记录的count累加
[recordNameCountDic setObject:@(nameCount) forKey:empName];
NSValue * value = rangeArr[nameCount-1];
range = [value rangeValue];
}else{
[recordNameCountDic setObject:@1 forKey:empName];
NSValue * value = rangeArr.firstObject;
range = [value rangeValue];
}
}

//根据range来记录id
NSValue *rangeKey = [NSValue valueWithRange:range];
[recordRangeEmpIdDic setObject:empId forKey:rangeKey];

[mAttributedString replaceCharactersInRange:range withAttributedString:lightNameAttributeStr];
}
mAttributedString.yy_lineSpacing = 2;
self.groupInfolabel.attributedText = mAttributedString;

以下是匹配一个字符串里包含字段某字符串的所有range

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSArray*)rangeOfSubString:(NSString*)subStr inString:(NSString*)string {
NSMutableArray *rangeArray = [NSMutableArray array];
NSString*string1 = [string stringByAppendingString:subStr];
NSString *temp;
for(int i =0; i < string.length; i ++) {
temp = [string1 substringWithRange:NSMakeRange(i, subStr.length)];
if ([temp isEqualToString:subStr]) {
NSRange range = {i,subStr.length};
[rangeArray addObject: [NSValue valueWithRange:range]];
}
}
return rangeArray;
}

正则过滤处理

将符合改正则表达式的字符串找出来,NSRegularExpression可以拿到NSTextCheckingResult,然后result可以拿到range.把对应的字符串截取出来。最后再循环遍替换成空字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 if([msgBody containsString:@"::"]){
//匹配替换掉::工号显示。
NSMutableArray *replaceFormatStrArr = [[NSMutableArray alloc] init];
NSError *error = nil;
NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:@"::\\d{1,}" options:NSRegularExpressionCaseInsensitive error:&error];
[regexp enumerateMatchesInString:msgBody options:0 range:NSMakeRange(0, msgBody.length) usingBlock:^(NSTextCheckingResult *result, __unused NSMatchingFlags flags, __unused BOOL *stop) {
if (flags != NSMatchingInternalError) {
NSRange firstHalfRange = [result rangeAtIndex:0];
if (firstHalfRange.length > 0) {
NSString *resultString1 = [msgBody substringWithRange:result.range];
[replaceFormatStrArr addObject:resultString1];
NSLog(@"result1 = %@",resultString1);
}
}
}];

for(NSString *formatStr in replaceFormatStrArr){
msgBody = [msgBody stringByReplacingOccurrencesOfString:formatStr withString:@""];
}
}

评论