0%

UITextView中#话题#功能的简单实现

实现过程1

今天来谈谈类似于新浪微博话题功能的简单实现,当文字是”#话题#”这种格式时,该文字字体得变颜色。个人觉得,这种问题的处理方式可以是,监听用户输入的信息,如果遇到有”#”号输入或删除时,再处理看是否需要改变字体颜色。于是我就按照这种思路写了一段改变颜色的代码,它就是是遍历textview.text,然后在将两个”#”号之间有文字的字体设置颜色,不会玩正则,所以这个方法比较蠢。O(∩_∩)O~

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
/**
* 设置textview字体属性
*/
- (void)setTextViewAttributed {
NSMutableArray *indexArray = [NSMutableArray array];
for (NSInteger i = 0; i < self.topicTextView.text.length; i++) {
NSString *indexString = [self.topicTextView.text substringWithRange:NSMakeRange(i, 1)];
if ([indexString isEqualToString:topicString]) {
[indexArray addObject:@(i)];
}
}
// reset
NSMutableAttributedString *aText = [[NSMutableAttributedString alloc] initWithString:self.topicTextView.text];
self.topicTextView.attributedText = aText;
self.topicTextView.font = [UIFont systemFontOfSize:16.0];

// change
if (indexArray.count > 1) {
NSMutableAttributedString *aText = [[NSMutableAttributedString alloc] initWithString:self.topicTextView.text];
for (NSInteger i = 0; i < indexArray.count; i++) {
NSInteger index1 = [indexArray[i] integerValue];
NSInteger index2 = 0;
if ((i + 1) < indexArray.count) {
index2 = [indexArray[i + 1] integerValue];
}
if (index2 - index1 > 1) {
// 多余中间有值才显示
[aText setAttributes:@{ NSForegroundColorAttributeName: TopicColor } range:NSMakeRange(index1, index2 - index1 + 1)];
++i;
}
}
self.topicTextView.attributedText = aText;
self.topicTextView.font = [UIFont systemFontOfSize:16.0];
}
}

实现过程2

这个是我一厢情愿写的demo。因为现实并不是这样子的,客户端的话题只能从服务端获取,每个话题都是有标识的,因为不能让用户随心所欲的创建话题撒!所以呢,用户自己手动输入”#”号,然并卵。如果是这样子的话,那我只有三个问题要解决了:

  • 改变话题字符串颜色;
  • 光标不能移动到话题字符串中间,当用户光标移动至话题后面时,用户第一次点击键盘删除按钮,其实是选中这个话题的,再一次点击键盘删除按钮时,才会删除这个话题字符串;
  • 上传至服务器的时候,并不是上传#话题#,而是上传协议好的字符串,就是能让服务端能够识别这个话题;

在实现过程中,我以AttributedString的颜色值为基准,用几个正则为查找工具,结合UITextView的三个代理方法。

1
2
3
4
5
6
7
8
9
/// Prior to replacing text, this method is called to give your delegate a chance to accept or reject the edits. If you do not implement this method, the return value defaults to YES.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;

/// The text view calls this method in response to user-initiated changes to the text. This method is not called in response to programmatically initiated changes. Implementation of this method is optional.
- (void)textViewDidChange:(UITextView *)textView;

/// Implementation of this method is optional. You can use the selectedRange property of the text view to get the new selection.
- (void)textViewDidChangeSelection:(UITextView *)textView;

这三个方法都不陌生吧,那么我的思路就简单了

  • shouldChangeTextInRange 代理方法中,第一,实现第一次选中,第二次删除功能;第二,实现插入话题后,需要改变其他字符串的初始颜色,得在这个方法里面做个标志。
  • textViewDidChange 代理方法中,实现 根据 shouldChangeTextInRange 方法中所得到的标志,设置字符串的初始颜色;
  • textViewDidChangeSelection 代理方法中,实现让光标不能移动到话题里面;

首先我定义了两个变量,插入了话题以后,继续在后面输入字符的话,字符颜色就跟话题颜色一样了。所以,我得用这两个变量来实现改变输入字符的初始颜色。

1
2
3
4
/// 改变Range
@property (assign, nonatomic) NSRange changeRange;
/// 是否改变
@property (assign, nonatomic) BOOL isChanged;

哦,对了,我还得用一个变量来记录上次光标所在的位置,因为话题字符串是不让它输入的。

1
2
/// 光标位置
@property (assign, nonatomic) NSInteger cursorLocation;

用户从其他界面选择好话题以后,它得插入到textview中啊:

1
2
3
4
5
NSString *insertText = [NSString stringWithFormat:@"#%@#", dict[KeyTopicName]];
[self.textView insertText:insertText];
NSMutableAttributedString *tmpAString = [[NSMutableAttributedString alloc] initWithAttributedString:self.textView.attributedText];
[tmpAString setAttributes:@{ NSForegroundColorAttributeName: TopicColor, NSFontAttributeName: DefaultSizeFont } range:NSMakeRange(self.textView.selectedRange.location - insertText.length, insertText.length)];
self.textView.attributedText = tmpAString;

然后我还得找到将用户所选择插入的话题位置啊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 得到话题Range数组
*
* @return return value description
*/
- (NSArray *)getTopicRangeArray:(NSAttributedString *)attributedString {
NSAttributedString *traveAStr = attributedString ?: _textView.attributedText;
__block NSMutableArray *rangeArray = [NSMutableArray array];
static NSRegularExpression *iExpression;
iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:@"#(.*?)#" options:0 error:NULL];
[iExpression enumerateMatchesInString:traveAStr.string
options:0
range:NSMakeRange(0, traveAStr.string.length)
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSRange resultRange = result.range;
NSDictionary *attributedDict = [traveAStr attributesAtIndex:resultRange.location effectiveRange:&resultRange];
if ([attributedDict[NSForegroundColorAttributeName] isEqual:TopicColor]) {
[rangeArray addObject:NSStringFromRange(result.range)];
}
}];
return rangeArray;
}

那么,三个UITextView delegate方法里的代码就可以这么玩了:

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
61
62
63
64
65
66
#pragma mark - UITextView Delegate
- (void)textViewDidChangeSelection:(UITextView *)textView {
NSArray *rangeArray = [self getTopicRangeArray:nil];
BOOL inRange = NO;
for (NSInteger i = 0; i < rangeArray.count; i++) {
NSRange range = NSRangeFromString(rangeArray[i]);
if (textView.selectedRange.location > range.location && textView.selectedRange.location < range.location + range.length) {
inRange = YES;
break;
}
}
if (inRange) {
textView.selectedRange = NSMakeRange(self.cursorLocation, textView.selectedRange.length);
return;
}
self.cursorLocation = textView.selectedRange.location;
}

- (void)textViewDidChange:(UITextView *)textView {
if (_isChanged) {
NSMutableAttributedString *tmpAString = [[NSMutableAttributedString alloc] initWithAttributedString:self.textView.attributedText];
[tmpAString setAttributes:@{ NSForegroundColorAttributeName: [UIColor blackColor], NSFontAttributeName: DefaultSizeFont } range:_changeRange];
_textView.attributedText = tmpAString;
_isChanged = NO;
}
}

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if ([text isEqualToString:@""]) { // 删除
NSArray *rangeArray = [self getTopicRangeArray:nil];
for (NSInteger i = 0; i < rangeArray.count; i++) {
NSRange tmpRange = NSRangeFromString(rangeArray[i]);
if ((range.location + range.length) == (tmpRange.location + tmpRange.length)) {
if ([NSStringFromRange(tmpRange) isEqualToString:NSStringFromRange(textView.selectedRange)]) {
// 第二次点击删除按钮 删除
return YES;
} else {
// 第一次点击删除按钮 选中
textView.selectedRange = tmpRange;
return NO;
}
}
}
} else { // 增加
NSArray *rangeArray = [self getTopicRangeArray:nil];
if ([rangeArray count]) {
for (NSInteger i = 0; i < rangeArray.count; i++) {
NSRange tmpRange = NSRangeFromString(rangeArray[i]);
if ((range.location + range.length) == (tmpRange.location + tmpRange.length) || !range.location) {
_changeRange = NSMakeRange(range.location, text.length);
_isChanged = YES;
return YES;
}
}
} else {
// 话题在第一个删除后 重置text color
if (!range.location) {
_changeRange = NSMakeRange(range.location, text.length);
_isChanged = YES;
return YES;
}
}
}
return YES;
}

好吧,通过以上方法,基本输入、删除操作功能是实现了。但是,上传貌似是个问题,因为我得上传跟服务端协议好的字符串格式啊;还有假如有一个聪明的用户,发现客户端和服务端的协议格式,他输入了那种格式的字符串,本着对服务端大哥的崇敬之心,我得将它转成”#话题#”格式啊。

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
/**
* 获取上传时 textview text 字符串
*
* @return <#return value description#>
*/
- (NSString *)getUploadString {
NSMutableString *lastTopic = [NSMutableString string];
NSAttributedString *formatAString = [self formatAttributedString];
NSArray *rangeArray = [self getTopicRangeArray:formatAString];
NSInteger lastLocation = 0;
for (NSInteger i = 0; i < rangeArray.count; i++) {
// 转成协议字符串代码
}
if (lastLocation < formatAString.string.length) {
[lastTopic appendString:[formatAString.string substringFromIndex:lastLocation]];
}
return lastTopic;
}

/**
* 上传时候 将文本中不带topic color的 协议字符串 改成 "#话题#" 上传至服务器
*
* @return <#return value description#>
*/
- (NSMutableAttributedString *)formatAttributedString {
NSMutableAttributedString *tmpAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:_textView.attributedText];
static NSRegularExpression *iExpression;
iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:@"获取协议字符串正则" options:0 error:NULL];
// 临时遍历的 topic数组
NSMutableArray *topicArray = [NSMutableArray array];
[topicArray addObjectsFromArray:[iExpression matchesInString:tmpAttributedString.string options:0 range:NSMakeRange(0, tmpAttributedString.string.length)]];
while (topicArray.count) {
NSTextCheckingResult *result = [topicArray firstObject];
NSString *searchStr = [tmpAttributedString.string substringWithRange:result.range];

/**
* 替换代码
*/

[topicArray removeAllObjects];

[topicArray addObjectsFromArray:[iExpression matchesInString:tmpAttributedString.string options:0 range:NSMakeRange(0, tmpAttributedString.string.length)]];
}
return tmpAttributedString;
}

实现总结

文中,我只说了大致的实现方式以及核心功能代码,因为有的东西涉及到公司的。。。所以,都懂得哈!
在做这个功能过程中,我走了一些弯路。因为在上次博文中,我延时的发现AttributedString可以设置图片,所以我当时的想法是将”#话题#”这段文字转成image然后添加到textview的AttributedString中。 牛逼的代码都找好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Text to Image
*
* @param text text description
*
* @return return value description
*/
- (UIImage *)imageFromText:(NSString *)text {
NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new];
paragraph.lineBreakMode = NSLineBreakByCharWrapping;
paragraph.alignment = NSTextAlignmentLeft;
NSDictionary *attributeDict = @{NSFontAttributeName: [UIFont systemFontOfSize:14.0], NSForegroundColorAttributeName: [UIColor redColor], NSParagraphStyleAttributeName: paragraph};
CGSize textSize = [text sizeWithAttributes:attributeDict];
NSAttributedString *mutableString = [[NSAttributedString alloc] initWithString:text attributes:attributeDict];
UIGraphicsBeginImageContextWithOptions(textSize, NO, 0.0);
[mutableString drawInRect:CGRectMake(0.0, 0.0, textSize.width, textSize.height)];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

可是。。。后面因为实现新浪微博,第一次删除选中,第二次删除删除的效果,这个思路被抛弃了。其实,这个思路当时还有两个问题,我还没有解决:

  • textview的AttributedString添加图片后,后面输入的文字和图片中的文字,centerY不相等,有偏差。
  • 如果#话题#转为图片了,我得用什么方法去获取它在最终上传字符串中的位置,因为添加完成后,用户可以在任意位置删除、添加新的字符。

在解决这个问题后,我觉得潜意识挺重要的,因为当时这个功能要在三天之内完成嘛,到了第二天标准下班时间时我还没弄出来,晚上10点多下班后,还是没有进展。当时,就紧张了,因为我没有解决这个问题,那我就成为了问题了撒。穷则变嘛,后面机智的我果断决定用最熟悉的笨方法解决,下班后我就一直在思考这个用textview的代理方法该怎么搞,结果第三天早晨我醒的特别早,并且一醒来还在想那个问题,那睁开眼睛的情形宛如电视剧男主角失忆后第一次睁开眼,好帅的感觉,O(∩_∩)O哈哈~。洗完脸后,我深深的体会到了那句话:每天叫醒你的不只是闹钟,还有八阿哥!!!其实,就是把问题复杂化了!!!(⊙o⊙)…