开源学习之StyledTextKit

StyledTextKit是一个简单的设置属性字符串的工具,使用builder模式构建,加入了上下文的概念,使得在创建属性字符串的时候能减少很多样板代码。

一个例子

先从使用入手,以下展示了如何创建一个属性字符串,其中Foo和baz!的样式是一样的,bar不同于他们:

其中使用到了StyledTextBuilder这个build类,它可以接收如下几种类型的参数,最终结果都是为数组变量styledTexts赋值。

接下来是save,这个方法是保存当前的环境,以便提供新的样式:

1
2
3
4
5
6
7
@discardableResult
public func save() -> StyledTextBuilder {
if let last = styledTexts.last?.style {
savedStyles.append(last)
}
return self
}

通过add这个函数来添加新的文本以及样式,和初始化一样,add操作也支持几种类型的参数,包括stringimageattributedString以及该框架的StyledText类,他们这些方法最后都会将参数映射成StyledText

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@discardableResult
public func add(styledTexts: [StyledText]) -> StyledTextBuilder {
self.styledTexts += styledTexts
return self
}

@discardableResult
public func add(styledText: StyledText) -> StyledTextBuilder {
return add(styledTexts: [styledText])
}

@discardableResult
public func add(style: TextStyle) -> StyledTextBuilder {
return add(styledText: StyledText(style: style))
}

@discardableResult
public func add(
storage: StyledText.Storage = .text(""),
traits: UIFontDescriptor.SymbolicTraits? = nil,
attributes: NSAttributedStringAttributesType? = nil
) -> StyledTextBuilder{...}

但是他们最终都是调用的这个函数:

这个函数内部的-add…方法会根据参数最后调用-add(styledTexts:)函数,为styledTexts数组添加一个元素。

与save成对的是restore,它可以跳出当前的样式上下文:

1
2
3
4
5
6
@discardableResult
public func restore() -> StyledTextBuilder {
guard let last = savedStyles.last else { return self }
savedStyles.removeLast()
return add(styledText: StyledText(style: last))
}

接着是build,build函数会根据初始化添加的styledTexts数组生成一个StyledTextString对象。

1
2
3
public func build(renderMode: StyledTextString.RenderMode = .trimWhitespaceAndNewlines) -> StyledTextString {
return StyledTextString(styledTexts: styledTexts, renderMode: renderMode)
}

在StyledTextString内部会遍历传入的styledTexts数组,在外部调用render方法的时候生成对应的NSAttributedString:

以上就是这个工具的简单使用,到现在为止,涉及到了以下几个类:

  • StyledTextBuilder
  • TextStyle(结构体)
  • Storage(枚举)
  • StyledText
  • StyledTextString(结构体)

简单的使用了下,发现它对于类的命名很乱,很多类容易产生歧义或者容易混淆。

StyledText

这个框架中提供了两个重要的角色:BuilderRender,他们根据文本或者图片进行构建然后渲染成属性字符串,这个过程可以用下面的图进行概括:

Builder

这个角色的抽象是StyledTextBuilder,主要负责对接业务方,比如为哪一些字符串、哪一个image设置对应的属性。主要的操作是为其中的styledTexts数组添加对应的StyledText元素,以及通过一个save-restore操作来修改对应的style。

styledTexts数组中的StyledText元素通过将数据和样式分离,抽象出来Storage枚举TextStyle类,这样处理起来会更加的灵活。这个数组是builder用来传递给下一个Render的重要数据,至于另外的一个savedStyles只不过是添加了一个save-restore特性,最终结果还是会作用到styledText数组中。

savedStyles会被用在两个api中:save()restore()。想象一个场景,包含姓名的字符串中,要将姓名做不同颜色、不同字体等处理,这个时候就可以使用save-restore这一个成对的api来完成,save之后执行add添加的字符串具备自己的样式,直到restore为止。

Render

他的抽象类是StyledTextString,需要通过builder传递过来装有StyledText的数组,然后根据设置的渲染模式渲染出来对应的属性字符串。

Render生成属性字符串的方法是render(contentSizeCategory:),根据styledTexts数组生成对应的属性字符串,然后根据自己的renderMode对这个字符串进行相应的修改,renderMode暂时可以不用考虑,只是作者考虑多情形的时候提供的功能。

由于styledTexts数组中装的是数据和样式的抽象:StyledText,因此,可以很快的生成对应的属性字符串,这一步是在StyledText的render(contentSizeCategory:)函数中完成的。styledTexts数组中的元素StyledText依次通过render方法生成的NSAttributedString会依次的追加到一个NSMutableAttributedString中,这个可变属性字符串就是业务方需要的最终结果。

可以看到,

  • 设置数据和样式并存储是在Build阶段完成的,也就是生成一系列的StyledText对象;
  • 渲染出对应的属性字符串是在Rende阶段完成的,根据一系列的StyledText对象依次执行render方法得到属性字符串。

Add

从上面的使用例子中大致理出来add的一系列方法之间的关系如下图所示,这些add方法的作用是为Builder的styledTexts数组增加新的元素,styledTexts数组会在之后的render阶段中使用。

这些add方法中最重要的是add(storage:)这一个,他会根据不同类型的数据来生成一个StyledText对象,通过源码发现内部使用一个Storage枚举一个TextStyle对象生成StyledText。Storage好说,就是函数的入参之一,后面TextStyle的创建才是这个函数的核心。

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
@discardableResult
public func add(
storage: StyledText.Storage = .text(""),
traits: UIFontDescriptor.SymbolicTraits? = nil,
attributes: NSAttributedStringAttributesType? = nil
) -> StyledTextBuilder {
guard let tip = styledTexts.last else { return self }

var nextAttributes = tip.style.attributes
if let attributes = attributes {
for (k, v) in attributes {
nextAttributes[k] = v
}
}

let nextStyle: TextStyle
if let traits = traits {

let tipFontDescriptor: UIFontDescriptor
switch tip.style.font {
case .descriptor(let descriptor): tipFontDescriptor = descriptor
default: tipFontDescriptor = tip.style.font(contentSizeCategory: .medium).fontDescriptor
}

nextStyle = TextStyle(
font: .descriptor(tipFontDescriptor.withSymbolicTraits(traits) ?? tipFontDescriptor),
size: tip.style.size,
attributes: nextAttributes,
minSize: tip.style.minSize,
maxSize: tip.style.maxSize
)
} else {
nextStyle = TextStyle(
font: tip.style.font,
size: tip.style.size,
attributes: nextAttributes,
minSize: tip.style.minSize,
maxSize: tip.style.maxSize
)
}

return add(styledText: StyledText(storage: storage, style: nextStyle))
}

Save-Restore

对一些特殊显示效果的字符串,框架提供的方式是一个成对的api:save()restore()

当执行save的时候,会从已经存在的styledTexts数组中找到最后的一个StyledText对象的style,也就是TextStyle类型的元素,然后将它放入到savesStyles数组中。

而restore函数内部会将savesStyles数组中的最后一个元素作为生成StyledText的参数,然后将新生成的StyledText对象放入styledTexts数组中,并且移除savesStyles数组中的最后一个元素。这里使用数组来完成状态保存的功能是为了便于多个save一起操作。

在save和restore函数中间,可以像往常一样执行add操作,添加对应的数据。

other

该框架还另外提供了一个StyledTextView,用来呈现生成的NSAttributedString,但是性能不是很高效,这里就没有对其进行进一步的探究了。

参考文章

  • 参考文章列表