在macOS原生开发中,想要实现专业级的文字排版,仅仅调用CTFontCreateWithName创建一个字体是远远不够的。高质量的西文排印往往依赖字体内置的OpenType特性,例如常见的连字(ligature)、旧式数字(oldstyle figures)、分数(fractions)以及小型大写字母(small caps)。Core Text提供了完整的CTFontFeature体系,让开发者可以细粒度地控制这些特性。本文将从原理到实践,详细讲解如何用CTFontFeature开启这些排版特性。

一、理解字体特性的底层原理
在深入API之前,需要先弄清楚什么是字体特性。现代字体文件(如OpenType字体)内部包含一张GSUB表(Glyph Substitution Table,字形替换表),它定义了在不同条件下某些字形序列应该被替换为其他字形。例如输入fi两个字符时,启用连字特性后渲染引擎会自动替换为一个整体字形,避免字母i上的点与f的弧线发生视觉冲突。
每个特性都有一个四字符的标签(tag),例如连字相关的标签是liga和rlig,旧式数字是onum,分数是frac,小型大写字母是smcp。Core Text通过CTFontFeature相关API把这些抽象的特性暴露给开发者。需要注意的是,并非所有字体都支持全部特性,只有字体本身实现了对应的GSUB规则,启用特性才会产生效果。举例来说,系统中著名的San Francisco字体对连字和分数支持良好,但小型大写字母的支持则取决于具体的字重和版本。
Core Text在macOS 10.15之后还提供了更现代的CTFontManagerCreateFontDescriptorFromData配合CTFontCopyFeatures的查询方式,可以枚举字体支持的全部特性及其可选值,这在需要动态适配字体的场景中非常实用。
二、查询字体支持的特性
在启用特性之前,先查询字体支持哪些特性是一个好习惯。Core Text提供了CTFontCopyFeatures函数,它返回一个包含字典的数组,每个字典描述一个特性组。下面是Objective-C的实现示例:
CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica"), 14.0, NULL);
CFArrayRef features = CTFontCopyFeatures(font);
if (features != NULL) {
for (CFIndex i = 0; i < CFArrayGetCount(features); i++) {
CFDictionaryRef feature = CFArrayGetValueAtIndex(features, i);
// 每个字典包含 kCTFontFeatureTypeIdentifierKey 等键
CFShow(feature);
}
CFRelease(features);
}
CFRelease(font);
返回的字典中,kCTFontFeatureTypeIdentifierKey标识特性类型,kCTFontFeatureTypeNameKey是人类可读的名称,而kCTFontFeatureTypeSelectorsKey下面则是可选值列表。旧式数字对应的类型标识是kLetterCaseType附近的数值组合,实际开发中更推荐直接使用常量名,例如kNumberCaseType与kLowerCaseNumbersSelector,这些常量定义在CoreText的头文件中,语义清晰且不易出错。
Swift开发者可以通过桥接调用同一套C API,也可以借助UIFontDescriptor的字符串属性来查询。不过要注意macOS上的NSFontDescriptor在解析特性字典时行为与iOS略有差异,跨平台项目中要做兼容测试。
三、启用连字与旧式数字的完整示例
启用特性的核心思路是:构建特性字典数组,把它设置到字体描述符(CTFontDescriptorRef)的kCTFontFeaturesAttribute属性上,再用这个描述符创建新字体。下面以Objective-C演示同时开启标准连字与旧式数字:
- (CTFontRef)fontWithFeatures {
// 定义要启用的特性:连字 + 旧式数字
NSArray *features = @[
@{ // 启用标准连字 (liga)
(id)kCTFontFeatureTypeIdentifierKey : @(kLigaturesType),
(id)kCTFontFeatureSelectorIdentifierKey : @(kCommonLigaturesOnSelector)
},
@{ // 启用旧式数字 (onum)
(id)kCTFontFeatureTypeIdentifierKey : @(kNumberCaseType),
(id)kCTFontFeatureSelectorIdentifierKey : @(kLowerCaseNumbersSelector)
}
];
NSDictionary *attributes = @{
(id)kCTFontNameAttribute : (id)CFSTR("Times New Roman"),
(id)kCTFontFeaturesAttribute : features
};
CTFontDescriptorRef descriptor =
CTFontDescriptorCreateWithAttributes((CFDictionaryRef)attributes);
return CTFontCreateWithFontDescriptor(descriptor, 18.0, NULL);
}
创建好的字体可以直接用于NSAttributedString的kCTFontAttributeName属性,绘制时Core Text会自动应用这些特性。验证效果很简单:输入包含fi、fl的英文单词观察是否变成单一字形,输入数字12345观察数字高度是否变成高低错落的形式。
分数与小型大写字母的开启方式完全相同,只需替换类型与选择器常量。分数使用kFractionsType配合kVerticalFractionSelector(对角分数,即斜杠形式)或kHorizontalFractionSelector(堆叠分数),小型大写字母使用kLetterCaseType配合kSmallCapsSelector。示例中改为斜分数的写法如下:
@{ // 启用对角分数 (frac)
(id)kCTFontFeatureTypeIdentifierKey : @(kFractionsType),
(id)kCTFontFeatureSelectorIdentifierKey : @(kVerticalFractionSelector)
},
@{ // 启用小型大写字母 (smcp)
(id)kCTFontFeatureTypeIdentifierKey : @(kLetterCaseType),
(id)kCTFontFeatureSelectorIdentifierKey : @(kSmallCapsSelector)
}
四、Swift实现与替代方案对比
在Swift中,除了直接调用C API,还可以使用NSFontDescriptor的特性字典,代码更为简洁:
let baseFont = NSFont(name: "Times New Roman", size: 18) ?? NSFont.systemFont(ofSize: 18)
let features: [[NSFontDescriptor.FeatureKey: Int]] = [
[.type: kLigaturesType, .selector: kCommonLigaturesOnSelector],
[.type: kNumberCaseType, .selector: kLowerCaseNumbersSelector],
[.type: kLetterCaseType, .selector: kSmallCapsSelector]
]
let descriptor = baseFont.fontDescriptor.addingAttributes([
.featureSettings: features
])
let styledFont = NSFont(descriptor: descriptor, size: 18)
两种方案的差异值得对比。基于CTFontDescriptor的方式更底层,适合需要精确控制Core Text绘制管线的场景,例如自定义文本布局引擎或跨iOS与macOS共享代码。基于NSFontDescriptor的方式则与AppKit的控件体系结合更好,直接把styledFont赋值给NSTextField等控件的font属性即可生效。
还有一种思路是在AttributedString中通过kCTFontFeatureSettingsAttribute按文本段落设置特性,粒度更细,适合同一行文字中混用比例数字与旧式数字的排版需求。三种方式底层最终都会转化为Core Text的run features,只是入口层级不同,开发者可以根据项目架构灵活选择。
五、实践中的注意事项
第一个常见问题是特性不生效。原因几乎都是字体不支持该特性,例如Helvetica本身没有真正的旧式数字字形,启用后渲染结果不会有任何变化。开发者应该在运行时用CTFontCopyFeatures做检测,或者引导用户选择支持完备的字体,如Times New Roman、Georgia、Hoefler Text等。
第二个问题是选择器冲突。某些特性组是互斥的,例如开启了小型大写字母又尝试开启标题大写,后设置的选择器可能覆盖前者。构建特性数组时要保证每个特性类型只出现一次,需要的选项以最后一次为准,必要时拆分为多个字体分别应用于不同的文本范围。
最后,在性能敏感的场景(如表格大量刷新)中,应缓存带有特性的CTFontRef,而不是每次绘制都重新构建字体描述符。字体描述符的创建涉及字典拷贝与字体匹配,频繁调用会带来可测量的开销。掌握了这些要点之后,配合Core Text的排版能力,就可以在macOS应用中呈现出媲美专业排版软件的文字效果了。
CTFontFeatureCore Text字体特性macOS字体排版修改时间:2026-09-01 12:44:38