WalkerZ's Blog

NSAttributeString 链式编程:WZAttributedStringChain

富文本 NSAttributedString 在开发中经常用得到,虽然很实用,但是代码写起来却并不那么漂亮:

1
2
3
4
5
NSMutableAttributedString *mutableAttrStr = [[NSMutableAttributedString alloc] init];
[mutableAttrStr appendAttributedString:[[NSAttributedString alloc] initWithString:str1 attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:26], NSForegroundColorAttributeName:[UIColor redColor]}]];
[mutableAttrStr appendAttributedString:[[NSAttributedString alloc] initWithString:str2]];
[mutableAttrStr setAttributes:@{NSForegroundColorAttributeName:[UIColor yellowColor], NSBackgroundColorAttributeName:[UIColor lightGrayColor]} range:NSMakeRange(str1.length, 2)];
[mutableAttrStr appendAttributedString:[[NSAttributedString alloc] initWithString:str3 attributes:@{NSForegroundColorAttributeName:[UIColor blueColor], NSWritingDirectionAttributeName:@[@(NSWritingDirectionRightToLeft | NSWritingDirectionOverride)]}]];

看着不怎么美观。所以长久以来总是想要封装一下 NSAttributedString,但是又没什么好的思路。

这两天突然想起了链式编程这个词,琢磨着要不玩一下呗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// BlockChainObject.h
@interface BlockChainObject : NSObject
@property (nonatomic, readonly) BlockChainObject *(^action)(void(^block)(void));
@end
// BlockChainObject.m
@interface BlockChainObject ()
@property (nonatomic, readwrite, copy) BlockChainObject *(^action)(void(^block)(void));
@end
@implementation BlockChainObject
- (instancetype)init {
self = [super init];
if (self) {
__weak typeof(self) weak_self = self;
self.action = ^BlockChainObject *(void(^block)(void)) {
block();
return weak_self;
};
}
return self;
}
@end
1
2
3
4
5
6
7
// 调用
BlockChainObject *chainObj = [[BlockChainObject alloc] init];
chainObj.action(^{
NSLog(@"1");
}).action(^{
NSLog(@"2");
});

挺好玩诶
可是要怎么应用到 NSAttributedString 上呢?
我们的目标是消除 NSAttributedStringKey,专注于 value 上,让代码能像这样写:

1
2
3
attrStr.range(0, 2).attributes(...).range(3, 4).attributes(...)
以及
attrStr.attributes(...)

封装思路

WZAttributesBuilder

为了消除 NSAttributedStringKey,专注于 value,我们需要将 NSAttributedStringKey 封装起来。为此我们需要创建一个类来装载这些 value:WZAttributesBuilder。

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
@interface WZAttributesBuilder : NSObject
@property (nonatomic, strong) UIFont *font; // 字体,默认 nil
@property (nonatomic, copy) NSParagraphStyle *paragraphStyle; // 段落风格,默认 nil
@property (nonatomic, strong) UIColor *foregroundColor; // 前景色(字体颜色),默认 nil
@property (nonatomic, strong) UIColor *backgroundColor; // 背景色,默认 nil
@property (nonatomic, assign) BOOL ligature; // 是否连字,默认 NO
@property (nonatomic, strong) NSNumber *kern; // 字符间距,默认 nil
@property (nonatomic, assign) NSUnderlineStyle strikethroughStyle; // 删除线样式,默认 NSUnderlineStyleNone,没有删除线
@property (nonatomic, strong) UIColor *strikethroughColor; // 删除线颜色,默认 nil
@property (nonatomic, assign) NSUnderlineStyle underlineStyle; // 下划线样式,同删除线
@property (nonatomic, strong) UIColor *underlineColor; // 下划线颜色,默认 nil
@property (nonatomic, strong) NSNumber *strokeWidth; // 笔画宽度,默认 nil
@property (nonatomic, strong) UIColor *strokeColor; // 笔画颜色,默认 nil
@property (nonatomic, strong) NSShadow *shadow; // 阴影样式,默认为 nil
@property (nonatomic, strong) NSTextEffectStyle textEffectStyle;// 特殊效果,默认为 nil
@property (nonatomic, strong) NSTextAttachment *attachment; // 附件,默认 nil
@property (nonatomic, strong) NSURL *link; // 链接属性,默认为 nil
@property (nonatomic, strong) NSNumber *baselineOffset; // 基线偏移量,默认 nil,正值上偏,负值下偏
@property (nonatomic, strong) NSNumber *obliqueness; // 字体倾斜量,默认 nil,正值右倾,负值左倾
@property (nonatomic, strong) NSNumber *expansion; // 字体的横向拉伸,默认 nil,正值拉伸,负值压缩
@property (nonatomic, assign) WZAttributedWriteDirection writeDirection; // 文字书写方向,默认 WZAttributedWriteDirectionNone
// 根据属性的值生成字典@{NSAttributedStringKey: value}
- (NSDictionary<NSAttributedStringKey, id> *)attributes;
@end
// implementation 就不写出来了,有点长,github 地址在最下面。

这里需要提的是 NSWritingDirectionAttributeName(书写方向)的值,因为这个值需要是@[@(NSWritingDirection | NSWritingDirectionFormatType)] 的形式,所以我自定义了一个枚举来封装这些可能的值:

1
2
3
4
5
6
7
8
// 文字书写方向,详细:[NSWritingDirectionAttributeName](https://developer.apple.com/documentation/uikit/nswritingdirectionattributename)
typedef NS_ENUM(NSUInteger, WZAttributedWriteDirection) {
WZAttributedWriteDirectionDefault, // 默认方向
WZAttributedWriteDirectionLRE, // Left to Right, Embed
WZAttributedWriteDirectionRLE, // Right to Left, Embed
WZAttributedWriteDirectionLRO, // Left to Right, Override
WZAttributedWriteDirectionRLO, // Right to Left, Override
};

测试之后发现只有 “Right to Left, Override” 的模式才能让书写方向由右往左书写,“Right to Left, Embed” 并不能达到这种效果。
苹果文档 对 NSWritingDirectionEmbedding 的解释:

Text is embedded in text with another writing direction. For example, an English quotation in the middle of an Arabic sentence could be marked as being embedded left-to-right text.
大概意思:一些文字以其他书写方向嵌入到一段文字中。比如一段阿拉伯文字中间嵌入一段英文文字,这段英文文字可以以从左向右的方向嵌入。

一开始没理解,查了一下阿拉伯文字的书写方向,发现是从右向左书写的。(= = )
虽然这么说,但是我并不知道该怎么测试 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄

WZAttributeRange

有了封装 NSAttributedStringKey 的类了,就要开始构思如何做到attrStr.range(0, 2).attributes(...)。我们可以认为attrStr.attributes(...)attrStr.range(0, attrStr.length).attributes(...)的简写形式。所以我们将围绕attrStr.range(0, 2).attributes(...)的形式来实现。

这里的问题只在于 range(0, 2) 应该返回什么值。1. 返回 NSAttributedString 对象,那么 NSAttributedString 对象需要保存一个 NSRange 变量;2. 返回一个第三方对象,那么这个对象需要保存一个 NSRange 变量,并实现 .attributes(...)

我这里用了第二种方法,至于为什么,因为我是稀里糊涂就做了的 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄。

因此我们创建 WZAttributeRange 类,具体实现:

1
2
3
4
5
6
7
8
9
10
11
// .h
typedef void(^WZBuildAttributesBlock)(WZAttributesBuilder *builder);
typedef NSAttributedString *(^WZAttributesBlock)(WZBuildAttributesBlock buildAttributes);
@interface WZAttributeRange : NSObject
- (instancetype)initWithAttributedString:(NSAttributedString *)attrStr range:(NSRange)range;
@property (nonatomic, readonly) WZAttributesBlock attributes;
@end

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
// .m
@interface WZAttributeRange ()
@property (nonatomic, strong) NSMutableAttributedString *mutableAttrStr;
@property (nonatomic, assign) NSRange range;
@property (nonatomic, readwrite, copy) WZAttributesBlock attributes;
@end
@implementation WZAttributeRange
- (instancetype)initWithAttributedString:(NSAttributedString *)attrStr range:(NSRange)range {
if (self = [super init]) {
self.mutableAttrStr = [attrStr mutableCopy];
self.range = range;
__weak typeof(self) weak_self = self;
[self setAttributes:^NSAttributedString *(WZBuildAttributesBlock buildAttributes) {
WZAttributesBuilder *builder = [[WZAttributesBuilder alloc] init];
buildAttributes(builder);
[weak_self.mutableAttrStr addAttributes:builder.attributes range:weak_self.range];
return [weak_self.mutableAttrStr copy];
}];
}
return self;
}
@end

这个类就这样,好像也没什么好说的了。需要注意的是,attributes()是用addAttributes 而不是 setAttributes
可能存在的问题大概是调用链比较长的时候,会频繁创建 WZAttributeRange 和 WZAttributesBuilder 对象。

NSAttributedString(WZAttributeChain)

材料都准备好了,就可以开始对 NSAttributedString 动刀了。我们需要两个最基本的东西:range(loc, len)attributes(...),考虑到经常需要拼接其他 attributedString,所以加多一个append(attributedString)

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
// .h
typedef WZAttributeRange *(^WZAttributeRangeBlock)(NSUInteger loc, NSUInteger len);
typedef NSAttributedString *(^WZAttributedAppendBlock)(NSAttributedString *attrStr);
@interface NSAttributedString (WZAttributeChain)
@property (nonatomic, readonly) WZAttributeRangeBlock range;
@property (nonatomic, readonly) WZAttributesBlock attributes;
@property (nonatomic, readonly) WZAttributedAppendBlock append;
@end
// .m
static const void *kWZAttributedStringRangeBlockKey = &kWZAttributedStringRangeBlockKey;
static const void *kWZAttributedStringAttributesBlockKey = &kWZAttributedStringAttributesBlockKey;
static const void *kWZAttributedStringAppendBlockKey = &kWZAttributedStringAppendBlockKey;
@implementation NSAttributedString (WZAttributeChain)
- (WZAttributeRangeBlock)range {
WZAttributeRangeBlock range = objc_getAssociatedObject(self, kWZAttributedStringRangeBlockKey);
if (!range) {
__weak typeof(self) weak_self = self;
range = ^WZAttributeRange *(NSUInteger loc, NSUInteger len){
WZAttributeRange *range = [[WZAttributeRange alloc] initWithAttributedString:weak_self range:NSMakeRange(loc, len)];
return range;
};
objc_setAssociatedObject(self, kWZAttributedStringRangeBlockKey, range, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
return range;
}
- (WZAttributesBlock)attributes {
WZAttributesBlock attributes = objc_getAssociatedObject(self, kWZAttributedStringAttributesBlockKey);
if (!attributes) {
__weak typeof(self) weak_self = self;
attributes = ^NSAttributedString *(WZBuildAttributesBlock buildAttributes) {
return weak_self.range(0, weak_self.length).attributes(buildAttributes);
};
objc_setAssociatedObject(self, kWZAttributedStringAttributesBlockKey, attributes, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
return attributes;
}
- (WZAttributedAppendBlock)append {
WZAttributedAppendBlock append = objc_getAssociatedObject(self, kWZAttributedStringAppendBlockKey);
if (!append) {
__weak typeof(self) weak_self = self;
append = ^NSAttributedString *(NSAttributedString *attrStr) {
NSMutableAttributedString *mutableAttrStr = [[NSMutableAttributedString alloc] initWithAttributedString:weak_self];
[mutableAttrStr appendAttributedString:attrStr];
return [mutableAttrStr copy];
};
objc_setAssociatedObject(self, kWZAttributedStringAppendBlockKey, append, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
return append;
}
@end

这里也好像没什么好说的,就是通过 runtime 给 NSAttributedString 添加三个 block 属性,并且实现它们。
不过这里没有做线程安全,感觉没什么必要。

这里存在的问题是 attrStr1.attributes(...) return attrStr2 attrStr1 和 attrStr2 已经不是同一个对象了,这点要注意!

如果需要返回对象是原来的对象,就需要用到 NSMutableAttributedString 了。因为工作机制不一样,所以我们需要通过 NSMutableAttributedString 的分类 WZAttributeChain 重新实现这三个 block。不过代码都差不多,就不贴出来了。

集成 WZAttributedStringChain

pod ‘WZAttributedStringChain’

WZAttributedStringChain 使用方法

假设我们要在 label 上显示下图这段富文本,这也是最上面那段原生 NSAttributedString 的输出结果:

label 输出

给定了三个字符串:

1
NSString *str1 = @"测试1", *str2 = @"测试2", *str3 = @"测试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
// NSAttributedString(WZAttributeChain)
label.attributedText =
str1.attributedString.attributes(^(WZAttributesBuilder *builder) {
builder.font = [UIFont systemFontOfSize:26];
builder.foregroundColor = [UIColor redColor];
}).append(str2.attributedString.range(0, 2).attributes(^(WZAttributesBuilder *builder) {
builder.foregroundColor = [UIColor yellowColor];
builder.backgroundColor = [UIColor lightGrayColor];
})).append(str3.attributedString.attributes(^(WZAttributesBuilder *builder) {
builder.foregroundColor = [UIColor blueColor];
builder.writeDirection = WZAttributedWriteDirectionRLO;
}));
// NSMutableAttributedString(WZAttributeChain)
NSString *str = [NSString stringWithFormat:@"%@%@%@", str1, str2, str3];
label.attributedText =
str.mutableAttributedString.range(0, 3).attributes(^(WZAttributesBuilder *builder) {
builder.font = [UIFont systemFontOfSize:26];
builder.foregroundColor = [UIColor redColor];
}).range(3, 2).attributes(^(WZAttributesBuilder *builder) {
builder.foregroundColor = [UIColor yellowColor];
builder.backgroundColor = [UIColor lightGrayColor];
}).range(6, 3).attributes(^(WZAttributesBuilder *builder) {
builder.foregroundColor = [UIColor blueColor];
builder.writeDirection = WZAttributedWriteDirectionRLO;
});

项目地址