01
什么是自标定体系,自标定体系是干什么用的
如上图所示,业务场景中存在评论、图文标题等富文本的编辑场景,一条文本中可能存在@的人、话题、链接等不同类型的内容。编辑结束如何告知下游服务文本中哪部分是特殊类型的内容,这些内容又分别是什么,就成了一个很棘手的问题。因为文本中插入特殊内容后文本还能继续编辑,如果在每次发生改动时更新记录的信息,几乎是无法实现的。如果编辑后采用正则匹配、文本匹配等方式识别,又会存在无法区分手输文本和插入的特殊内容的问题。
所以,如果富文本中的每个特殊内容上都标记了这段是什么内容,那不管怎么编辑,只要从头到尾扫描一次标记就可以获取到所有特殊内容的范围、类型、对应数据,如下图所示:
这就是自标定体系,用于富文本类型内容的生产编辑。
02
自标定体系是如何实现的
@interface NSMutableAttributedString (NSExtendedMutableAttributedString)
@property (readonly, retain) NSMutableString *mutableString;
- (void)addAttribute:(NSAttributedStringKey)name value:(id)value range:(NSRange)range;
- (void)addAttributes:(NSDictionary<NSAttributedStringKey, id> *)attrs range:(NSRange)range;
- (void)removeAttribute:(NSAttributedStringKey)name range:(NSRange)range;
- (void)replaceCharactersInRange:(NSRange)range withAttributedString:(NSAttributedString *)attrString;
- (void)insertAttributedString:(NSAttributedString *)attrString atIndex:(NSUInteger)loc;
- (void)appendAttributedString:(NSAttributedString *)attrString;
- (void)deleteCharactersInRange:(NSRange)range;
- (void)setAttributedString:(NSAttributedString *)attrString;
- (void)beginEditing;
- (void)endEditing;
@end
iOS系统中富文本对应的 API NSMutableAttributedString,可以通过 - (void)addAttributes:(NSDictionary<NSAttributedStringKey, id> *)attrs range:(NSRange)range 方法给 range 标定的字符绑定字典,系统通过这种方式实现富文本中的行间距、高亮、附件等功能:
// Predefined character attributes for text. If the key is not present in the dictionary, it indicates the default value described below.
UIKIT_EXTERN NSAttributedStringKey const NSFontAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // UIFont, default Helvetica(Neue) 12
UIKIT_EXTERN NSAttributedStringKey const NSParagraphStyleAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // NSParagraphStyle, default defaultParagraphStyle
UIKIT_EXTERN NSAttributedStringKey const NSForegroundColorAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // UIColor, default blackColor
UIKIT_EXTERN NSAttributedStringKey const NSBackgroundColorAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // UIColor, default nil: no background
UIKIT_EXTERN NSAttributedStringKey const NSLigatureAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // NSNumber containing integer, default 1: default ligatures, 0: no ligatures
UIKIT_EXTERN NSAttributedStringKey const NSKernAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // NSNumber containing floating point value, in points; amount to modify default kerning. 0 means kerning is disabled.
UIKIT_EXTERN NSAttributedStringKey const NSTrackingAttributeName API_AVAILABLE(macos(11.0), ios(14.0), watchos(7.0), tvos(14.0), visionos(1.0)); // NSNumber containing floating point value, in points; amount to modify default tracking. 0 means tracking is disabled.
UIKIT_EXTERN NSAttributedStringKey const NSStrikethroughStyleAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // NSNumber containing integer, default 0: no strikethrough
UIKIT_EXTERN NSAttributedStringKey const NSUnderlineStyleAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // NSNumber containing integer, default 0: no underline
UIKIT_EXTERN NSAttributedStringKey const NSStrokeColorAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // UIColor, default nil: same as foreground color
UIKIT_EXTERN NSAttributedStringKey const NSStrokeWidthAttributeName API_AVAILABLE(macos(10.0), ios(6.0), tvos(9.0), watchos(2.0), visionos(1.0)); // NSNumber containing floating point value, in percent of font point size, default 0: no stroke; positive for stroke alone, negative for stroke and fill (a typical value for outlined text would be 3.0)
...
实际上 NSAttributedStringKey 就是 NSString 的别名,
typedef NSString * NSAttributedStringKey NS_TYPED_EXTENSIBLE_ENUM;
也就是说,NSMutableAttributedString 自带一个跟文本绑定的 NSString 为 key 的存储空间,那我们只需要自定义一个 NSString 的 Key 用来存取标记数据就可以了。
YYKit 中声明了一个 YYTextBackedStringAttributeName,
NSString *const YYTextBackedStringAttributeName = @"YYTextBackedString";
用来存取 YYTextBackedString:
/**
YYTextBackedString objects are used by the NSAttributedString class cluster
as the values for text backed string attributes (stored in the attributed
string under the key named YYTextBackedStringAttributeName).
It may used for copy/paste plain text from attributed string.
Example: If :) is replace by a custom emoji (such as😊), the backed string can be set to @":)".
*/
@interface YYTextBackedString : NSObject <NSCoding, NSCopying>
+ (instancetype)stringWithString:(nullable NSString *)string;
@property (nullable, nonatomic, copy) NSString *string; ///< backed string
@end
YYTextBackedString 乍一看没有什么用,只能存字符,但是我们可以通过子类扩展属性,增加可存储的数据类型,同时子类的类名就代表了内容的类型:
//@的人
@interface SVAtExtraTextBackedString : YYTextBackedString
@property (nonatomic, strong) AtExtraInfoModel *atExtraInfo;
@end
//Seek时间点
@interface SVCommentSeekTextBackedString : YYTextBackedString
@property (nonatomic, strong) CommentSeekInfoModel *seekInfo;
@end
//链接
@interface SVCommentLinkTextBackedString : YYTextBackedString
@property (nonatomic, strong) CommentLinkInfoModel *linkInfo;
@end
03
标定内容如何解析
以@的人为例,用 YYTextBackedStringAttributeName 从前向后遍历,如果是 SVAtExtraTextBackedString 类型就是@的人的数据,相应的range就是“@xxx”文本所在的区域:
- (NSArray<AtExtraInfoModel *> *)parseAtExtraInfoModels {
NSMutableArray *models = [NSMutableArray array];
[self enumerateAttribute:YYTextBackedStringAttributeName inRange:self.yy_rangeOfAll options:kNilOptions usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) {
SVAtExtraTextBackedString *backedString = [value as:SVAtExtraTextBackedString.class];
if (backedString && backedString.atExtraInfo) {
NSInteger location = [self yy_plainTextForRange:NSMakeRange(0, range.location)].length;
backedString.atExtraInfo.range = NSMakeRange(location, range.length);
[models addObject:backedString.atExtraInfo];
}
}];
return [models copy];
}
04
动态识别与自标定混用
业务中,图文标题的生产场景,话题类的文本不是点击添加产生的,而是手输自动高亮,通过正则识别:
#[^#\s]* //未完成话题
#[^#\s]+\s{1} //完成话题
这样 “#时间没有绝对@小狐狸210757605” 整段都会被识别为话题,@的信息就和话题冲突了,可以通过查询正则识别区域内是否有标记信息来解决这个问题。以 “图文标题#时间没有绝对@小狐狸210757605” 为例,(location,length)[4,20] 范围内为 “#时间没有绝对@小狐狸210757605” ,查询 (location,length)[4,20] 范围内的 SVAtExtraTextBackedString 为 (location,length)[11,13],所以话题的实际范围应该到@的人前面为 (location,length)[4,7]:
@objc func finishedTopicMatchResults(inRange range: NSRange) -> [NSTextCheckingResult] {
guardlet currentText = self.attributedText, currentText.length > 0else { return [NSTextCheckingResult]() }
guard currentText.yy_rangeOfAll().contains(range.lowerBound) && currentText.yy_rangeOfAll().contains(range.upperBound-1) else {
return [NSTextCheckingResult]()
}
do {
let pattern = "#[^#\\s]+\\s{1}"
let regex = tryNSRegularExpression(pattern: pattern)
let matches = regex.matches(in: currentText.string, options: [], range: range)
var finalMatches = [NSTextCheckingResult]()
if matches.count > 0 {
let attributeKey = NSAttributedString.Key(rawValue: YYTextBackedStringAttributeName)
for matchResult in matches {
var currentRange = matchResult.range
var matchCouldUse = true
currentText.enumerateAttribute(attributeKey, in: currentRange, options: []) { value, range, stop in
iflet_ = value as? SVAtExtraTextBackedString {
if currentRange.contains(range.location) {
currentRange = NSRange(location: currentRange.location, length: range.location - currentRange.location)
if currentText.yy_rangeOfAll().contains(currentRange.lowerBound) && currentText.yy_rangeOfAll().contains(currentRange.upperBound-1) {
let topicStr = currentText.attributedSubstring(from: currentRange).string
if topicStr.contains(" ") == false {
matchCouldUse = false
}
}
stop.pointee = true
}
}
}
if matchCouldUse {
finalMatches.append(matchResult)
}
}
}
return finalMatches
} catch {
return [NSTextCheckingResult]()
}
}

936

被折叠的 条评论
为什么被折叠?



