可变字体(Variable Fonts)允许在单一字体文件中包含多个设计轴,例如字重、字宽、斜体和光学尺寸。在 macOS 上,Core Text 框架通过 CTFontVariation 字典来暴露这些轴的控制能力。与传统的静态字体相比,可变字体不仅减少了字体资源的体积,还提供了更平滑的样式过渡体验。本文将详细讲解如何在 Core Text 中获取变体信息、修改变体值,并讨论实际开发中可能遇到的坑。

可变字体与 Core Text 基础
OpenType 可变字体通过定义多个设计轴来描述字体的可变化维度。每个轴都有一个四字符标签(Tag),例如 wght 代表字重、wdth 代表字宽、ital 代表斜体、opsz 代表光学尺寸。Core Text 使用 CTFontRef 来表示一个具体的字体实例,而 CTFontVariation 是一个 CFDictionary 类型的字典,其中键是轴的标签字符串,值是对应的 NSNumber 或 CFNumber 数值。
要操作可变字体,首先需要一个支持变体的 CTFont 对象。可以通过 CTFontCreateWithName 创建默认实例,然后使用 CTFontCreateCopyWithVariation 函数来生成带有新变体值的字体副本。下面的 Swift 代码展示了如何获取系统的可变字体并创建一个更粗的字重实例。
import CoreText
// 获取一个支持可变字体的系统字体,例如 SF Pro
let fontName = ".AppleSystemUIFont" as CFString
let baseFont = CTFontCreateWithName(fontName, 17.0, nil)
// 获取原始字体描述中的变体字典
let originalVariation = CTFontCopyVariation(baseFont) as? [String: Any]
print("原始变体: \(String(describing: originalVariation))")
// 修改字重轴(wght),取值范围通常为 100-900
var newVariation = originalVariation ?? [:]
newVariation["wght"] = 700.0
// 创建带有新变体值的字体副本
if let boldFont = CTFontCreateCopyWithVariation(baseFont, 17.0, nil, newVariation as CFDictionary) {
// 使用 boldFont 进行绘制
print("成功创建粗体变体")
}
需要注意的是,CTFontCopyVariation 返回的字典可能包含多个轴,修改时应当保持其他轴的值不变,只更新需要调整的部分。另外,不同字体支持的轴范围不同,超出范围的值可能导致意外的渲染结果。
控制字重、字宽、斜体与光学尺寸
可变字体的核心应用场景在于动态调整这几个关键视觉属性。Core Text 并没有为每个轴提供单独的 API,而是统一通过变体字典进行控制。对于字重轴 wght,取值范围通常是 100 到 900,数值越大字体越粗。字宽轴 wdth 的取值范围一般是 75 到 125,表示相对于正常宽度的百分比。斜体轴 ital 的值通常在 0 到 1 之间,0 表示直立,1 表示完全斜体。光学尺寸轴 opsz 影响字体在不同字号下的细节表现,例如在小字号时简化笔画、在大字号时增强对比度。
实际开发中,我们需要先确认当前字体是否包含目标轴。可以通过 CTFontCopyVariationAxes 函数获取所有轴的描述信息,包括轴标识、最小值和最大值。下面的代码展示了如何枚举字体的所有变体轴,并打印每个轴的四字符标签和范围。
import CoreText
let font = CTFontCreateWithName("HelveticaNeue" as CFString, 18.0, nil)
if let axes = CTFontCopyVariationAxes(font) as? [[String: Any]] {
for axis in axes {
if let identifier = axis[kCTFontVariationAxisIdentifierKey as String] as? NSNumber,
let minValue = axis[kCTFontVariationAxisMinimumValueKey as String] as? NSNumber,
let maxValue = axis[kCTFontVariationAxisMaximumValueKey as String] as? NSNumber {
let tag = identifier.uint32Value
let bytes = [UInt8((tag >> 24) & 0xFF), UInt8((tag >> 16) & 0xFF), UInt8((tag >> 8) & 0xFF), UInt8(tag & 0xFF)]
let tagString = String(bytes: bytes, encoding: .ascii) ?? "????"
print("轴: \(tagString), 最小值: \(minValue), 最大值: \(maxValue)")
}
}
}
上述代码中的 kCTFontVariationAxisIdentifierKey 等常量在 Swift 中需要转换成 String 使用。不同字体可能包含不同的轴,例如系统字体 SF Pro 支持 wght、wdth、opsz 等多个轴,而第三方字体可能只支持部分轴。开发者应当做好兜底处理,避免在字典中设置不存在的键导致行为异常。
当同时调整多个轴时,建议一次性传入包含所有目标变体的字典,而不是多次调用 CTFontCreateCopyWithVariation。这样做可以减少字体对象的创建次数,提高性能。另外,在 AppKit 中,可以直接将带有变体的 CTFont 转换为 NSFont,方便与现有文本系统集成。
实际应用与高级技巧
在实际的 macOS 应用中,可变字体可以用于实现平滑的字体动画,比如滑动滑块时动态改变字重或字宽。由于每次调整都会生成新的字体对象,频繁调用可能会带来一定的性能开销。一种优化方式是缓存最近使用的字体实例,或者利用 NSFontDescriptor 的匹配机制来获取系统字体。
另一个常见场景是响应式排版。通过将光学尺寸轴 opsz 与显示字号绑定,可以让字体在较小尺寸下保持清晰易读,在较大尺寸下展现更多设计细节。例如,在绘制文本之前,根据目标字号设置相应的 opsz 值,然后创建对应的字体实例。下面的代码展示了如何根据字号动态调整光学尺寸。
import CoreText
func fontWithOpticalSize(fontName: String, baseSize: CGFloat, targetSize: CGFloat) -> CTFont? {
let font = CTFontCreateWithName(fontName as CFString, targetSize, nil)
var variation = CTFontCopyVariation(font) as? [String: Any] ?? [:]
// opsz 值通常与字号接近,但具体范围取决于字体设计
variation["opsz"] = targetSize
return CTFontCreateCopyWithVariation(font, targetSize, nil, variation as CFDictionary)
}
if let font = fontWithOpticalSize(fontName: "SFProText-Regular", baseSize: 17.0, targetSize: 48.0) {
// 使用该字体绘制标题或大号文字
print("已生成适配 48pt 的光学尺寸字体")
}
跨平台兼容性也需要留意。iOS 上的 Core Text 同样支持 CTFontVariation,但系统内置的可变字体种类可能不同。对于自定义字体,需要确保字体文件本身包含所需的变体轴,否则设置变体值不会产生任何效果。开发者可以通过第三方工具如 fonttools 检查字体文件的变体表。
最后提醒一点,不要将 CTFontVariation 与传统的字体特征(如 kCTFontWeightTrait)混淆。前者直接控制可变字体轴,后者是基于字体特征的匹配机制,两者工作层次不同。理解这些底层概念,能够帮助你在处理字体渲染问题时更加游刃有余。
Core TextCTFontVariation可变字体轴修改时间:2026-09-18 05:15:01