mcpField
A
axiom-typography-ref

axiom-typography-ref

github.com/charleswiltgen设计与 UI1.2k175 次安装今天更新
代码仓库

关于 axiom-typography-ref

面向 Apple 平台界面设计与开发者的排版参考技能,汇总系统字体、文本样式、Dynamic Type、行距、字距及国际化要点,并提供 SwiftUI 和 UIKit 示例。

主要功能

  1. 01介绍 Apple 系统字体系列
  2. 02说明文本样式与 Dynamic Type
  3. 03调整字距、行距和行高
  4. 04处理 AttributedString 排版
  5. 05涵盖多语言文本布局

适用场景

  1. 01为界面选择合适的字体样式
  2. 02让自定义字体随 Dynamic Type 缩放
  3. 03微调段落行高和字距
  4. 04排查富文本样式被覆盖的问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-typography-ref

安装后在 Claude Code 中输入/axiom-typography-ref确认已加载。

详细介绍

字体排印参考

Apple 平台字体排印完整参考,涵盖 San Francisco 字体系统、文本样式、动态字体大小、字距、行距,以及截至 iOS 26 的国际化。

San Francisco 字体系统

字体系列

SF Pro 和 SF Pro Rounded(iOS、iPadOS、macOS、tvOS)

  • 大多数 UI 元素使用的主要系统字体
  • 圆角变体适用于友好、亲切的界面(例如“提醒事项”App)

SF Compact 和 SF Compact Rounded(watchOS、窄栏)

  • 针对受限空间和较小字号进行优化
  • watchOS 默认系统字体

SF Mono(代码环境、等宽文本)

  • 适用于代码编辑器和技术内容的等宽字体
  • 字符宽度一致,便于对齐

New York(系统衬线字体)

  • 适用于编辑类内容的衬线字体替代方案
  • 与 SF Pro 一样支持文本样式
可变字体轴
字重轴(9 种字重)
  • Ultralight、Thin、Light、Regular、Medium、Semibold、Bold、Heavy、Black
  • 通过可变字体实现连续的字重范围
  • 小字号时避免使用较细字重(会影响易读性)
字宽轴(WWDC 2022)
  • Condensed — 最窄
  • Compressed — 较窄
  • Regular — 标准宽度(默认)
  • Expanded — 较宽

通过以下方式访问:

// iOS/macOS
let descriptor = UIFontDescriptor(fontAttributes: [
    .family: "SF Pro",
    kCTFontWidthTrait: 1.0 // 1.0 = Expanded
])

SF Arabic(WWDC 2022)

  • 阿拉伯语文本采用与 SF Pro 一致的设计风格
  • 正确支持从右到左的排版
光学字号

可变字体会根据字号自动调整光学尺寸:

  • Text 变体(< 20pt)— 字距更大、笔画更粗壮
  • Display 变体(≥ 20pt)— 字距更紧凑、细节更精致
  • 使用可变 SF Pro 时可平滑过渡(17-28pt)

来自 WWDC 2020:

“TextKit 2 抽象了字形处理,从而为国际文本提供一致的体验。”

文本样式与动态字体大小

系统文本样式
文本样式默认字号(iOS)用途
.largeTitle34pt主要页面标题
.title28pt次级标题
.title222pt第三级标题
.title320pt第四级标题
.headline17pt(Semibold)强调正文
.body17pt主要正文
.callout16pt次要正文
.subheadline15pt第三级正文
.footnote13pt脚注、说明文字
.caption12pt小号注释
.caption211pt最小号注释
字号指南
  • 避免将 .caption2 用于需要易读的内容 — 11pt 适合时间戳和元数据注释,但对于正文或用户需要阅读的标签来说太小。易读内容的最小字号建议使用 .caption 或 .footnote。
强调文本样式

应用 .bold 符号特征以获取强调变体:

// UIKit
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1)
let boldDescriptor = descriptor.withSymbolicTraits(.traitBold)!
let font = UIFont(descriptor: boldDescriptor, size: 0)

// SwiftUI
Text("Bold Title")
    .font(.title.bold())

各文本样式的实际字重:

  • 某些样式映射为 medium
  • 其他样式映射为 semibold、bold 或 heavy
  • 具体取决于语义层级
行距变体

紧凑行距(iOS 行高减少 2pt,watchOS 减少 1pt):

// UIKit
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
let tightDescriptor = descriptor.withSymbolicTraits(.traitTightLeading)!

// SwiftUI
Text("Compact text")
    .font(.body.leading(.tight))

宽松行距(iOS 行高增加 2pt,watchOS 增加 1pt):

// SwiftUI
Text("Spacious paragraph")
    .font(.body.leading(.loose))
动态字体大小

自动缩放(iOS): 文本样式会根据用户在“设置”→“显示与亮度”→“文字大小”中的偏好自动缩放。

支持动态字体大小的自定义字体:

// UIKit - UIFontMetrics
let customFont = UIFont(name: "Avenir-Medium", size: 34)!
let bodyMetrics = UIFontMetrics(forTextStyle: .body)
let scaledFont = bodyMetrics.scaledFont(for: customFont)

// Also scale constants
let spacing = bodyMetrics.scaledValue(for: 20.0)
// SwiftUI - .font(.custom(_:relativeTo:))
Text("Custom scaled text")
    .font(.custom("Avenir-Medium", size: 34, relativeTo: .body))

// @ScaledMetric for values
@ScaledMetric(relativeTo: .body) var padding: CGFloat = 20
平台差异

macOS

  • AppKit 不支持动态字体大小
  • 文本样式字号针对 macOS 控件尺寸进行了优化
  • Catalyst App 使用 iOS 字号 × 77%(旧版),或 macOS 优化字号(“为 Mac 优化界面”)

watchOS

  • 较小的文本样式针对表盘进行了优化
  • 紧凑显示屏默认使用紧凑行距

visionOS

  • 系统字体的工作方式与 iOS 完全相同
  • 支持动态字体大小

字距与行距

字距(字母间距)

字距用于调整字母之间的间距,是实现光学字号效果的关键。

按字号区分的字距表:

SF Pro 包含随字号变化的字距值,以保持最佳间距:

  • 较大字号:字距更紧
  • 较小字号:字距更宽

Apple Design Resources 中的示例:

  • 34pt(largeTitle):字距 +0.016
  • 17pt(body):字距 +0.008
  • 11pt(caption2):字距 +0.06

紧凑字距 API(用于适配文本):

// UIKit
textView.allowsDefaultTightening(for: .byTruncatingTail)

// SwiftUI
Text("Long text that needs to fit")
    .lineLimit(1)
    .minimumScaleFactor(0.5) // Allows tight tracking

手动设置字距:

// UIKit
let attributes: [NSAttributedString.Key: Any] = [
    .font: UIFont.preferredFont(forTextStyle: .body),
    .kern: 2.0 // 2pt tracking
]

// SwiftUI
Text("Tracked text")
    .tracking(2.0)
    .kerning(2.0) // Alternative API

重要: 为确保语义正确,请使用 .tracking(),而不是 .kerning() API。必要时,字距会禁用连字;字偶间距则不会。

行距(行间距)

默认行高: 根据字体内置度量计算(升部 + 降部 + 行间距)。

针对语言的调整: iOS 17+ 会自动增加具有较高升部/降部的文字系统的行高:

  • 阿拉伯语
  • 泰语、老挝语
  • 印地语、孟加拉语、泰卢固语

摘自 WWDC 2023:

“针对高度可变的文字系统自动调整行高”

手动设置行距:

// UIKit
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 8.0 // 8pt additional space

// SwiftUI (iOS 13+)
Text("Custom spacing")
    .lineSpacing(8.0)

行高(iOS 26+):

.lineHeight() 直接设置基线到基线的距离,比 .lineSpacing()(测量从底部到顶部的距离)更直观。

// Presets
Text("Open layout").lineHeight(.loose)
Text("Compact layout").lineHeight(.tight)

// Precise control
Text("Scaled").lineHeight(.multiple(factor: 1.5))
Text("Fixed").lineHeight(.exact(points: 30)) // Does NOT scale with Dynamic Type

对于设置了样式的字符串,也可以使用 AttributedString.lineHeight。完整 API 详情请参阅 axiom-swiftui-26-ref。

第三方字体字距

iOS 18 新增: 字体供应商可以使用 STAT 表和 CTFont 光学字号属性,在自定义字体中嵌入字距表。

let attributes: [String: Any] = [
    kCTFontOpticalSizeAttribute as String: pointSize
]
let descriptor = CTFontDescriptorCreateWithAttributes(attributes as CFDictionary)
let font = CTFontCreateWithFontDescriptor(descriptor, pointSize, nil)

SwiftUI AttributedString 排版

字体环境交互

关键模式 使用 AttributedString 搭配 SwiftUI 的 Text 时,如果字体来自环境而非属性内容,段落样式(如 lineHeightMultiple)可能会丢失。

摘自 WWDC 2025-280:

“对于任何值为 nil 的 AttributedStringKeys,TextEditor 都会用根据环境计算出的默认值替代。”

同样的原则也适用于 Text——当 AttributedString 未指定字体时,SwiftUI 会应用环境字体,这可能导致它重建文本片段,并丢弃或规范化段落样式细节。

问题
// ❌ WRONG - .font() modifier can override and drop paragraph styles
var s = AttributedString(longString)

// Set paragraph style
var p = AttributedString.ParagraphStyle()
p.lineHeightMultiple = 0.92
s.paragraphStyle = p
// ⚠️ No font set in AttributedString

Text(s)
    .font(.body) // ⚠️ May rebuild runs, lose lineHeightMultiple

失败原因:

  1. AttributedString 未设置字体属性(值为 nil)
  2. SwiftUI 的 .font(.body) 修饰符会告知它“对整个文本片段使用此字体”
  3. SwiftUI 使用环境字体重建文本片段
  4. 重建过程中,段落样式会被丢弃或规范化
解决方案

需要精细控制时,请将排版信息保留在 AttributedString 中:

// ✅ CORRECT - Font in AttributedString, no environment override
var s = AttributedString(longString)

// Set font INSIDE the attributed content
s.font = .system(.body) // ✅ Typography inside AttributedString

// Set paragraph style
var p = AttributedString.ParagraphStyle()
p.lineHeightMultiple = 0.92
s.paragraphStyle = p

Text(s) // ✅ No .font() modifier

有效原因:

  1. 字体属于属性内容的一部分(而非 nil)
  2. 不会受到 .font() 修饰符的环境覆盖
  3. SwiftUI 会保留字体和段落样式
  4. 文本片段及其所有属性都会保持完整
何时使用哪种方式
在 AttributedString 中使用字体(精细控制)
var s = AttributedString("Carefully styled text")
s.font = .system(.body)

var p = AttributedString.ParagraphStyle()
p.lineHeightMultiple = 0.92
p.alignment = .leading
s.paragraphStyle = p

Text(s) // No modifier

适用场景:

  • 需要精确设置段落样式(行高、对齐方式)
  • 在一个字符串中混用多种字体
  • 内容将在 Text 和 TextEditor 中显示
  • 保留富文本编辑器中的精确格式
使用 .font() 修饰符(全局覆盖)
Text("Simple text")
    .font(.body)
    .lineSpacing(4.0) // SwiftUI-level spacing

适用场景:

  • 不带段落样式的简单文本
  • 需要动态字体自动缩放
  • 需要 SwiftUI 的语义字体行为(深色模式、辅助功能)
  • 有意覆盖 AttributedString 字体
在一个字符串中使用多种字体
var s = AttributedString("Title")
s.font = .system(.title).bold()

var body = AttributedString(" and body text")
body.font = .system(.body)

s.append(body)

Text(s) // ✅ No .font() modifier preserves both fonts
常见误区:顺序无关紧要
// ❌ WRONG mental model: "Create AttributedString first"
var s = AttributedString(text)
var p = AttributedString.ParagraphStyle()
p.lineHeightMultiple = 0.92
s.paragraphStyle = p
s.font = .system(.body) // ⚠️ Setting font last doesn't help if you use .font() modifier

Text(s).font(.body) // Still breaks!

问题不在于你何时在 AttributedString 中设置字体,而在于属性内容是否包含自己的字体属性,而不是依赖 SwiftUI 的 .font(...) 环境。

验证清单

使用 AttributedString 设置段落样式时:

  • 在 AttributedString 中设置字体(而不是 nil)
  • 不对 Text 视图使用 .font() 修饰符(除非有意覆盖)
  • 在字体之前或之后设置段落样式(顺序无关)
  • 使用实际内容进行测试,确认行高和对齐方式得以保留

国际化

双向文本

复杂文字示例(摘自 WWDC 2021):

卡纳达语单词“October”:

  • 字符索引 4 处的分裂元音会对应 2 个字形
  • 连字应用前会对字形重新排序
  • 字形索引 ≠ 字符索引

这就是 TextKit 2 使用 NSTextLocation 而非整数索引的原因。

希伯来语/阿拉伯语选择: 由于从右向左的排版,单个视觉选区在 AttributedString 中会对应多个 NSRange。

换行

语言感知(iOS 17+):

  • 中文、日文、韩文:在语义边界处换行
  • 德语:避免拆分复合词
  • 英语:优先在连字符处换行

均匀换行(TextKit 2): 两端对齐的段落采用改进的换行算法:

  • 减少行间距过大的情况
  • 让单词间距更均匀
  • TextKit 2 中自动启用
防止文本裁切

最佳实践:

  1. 使用动态字体(自动调整)
  2. 在 SwiftUI 中设置 .lineLimit(nil) 或 .lineLimit(2...5)
  3. 对受限的单行文本使用 .minimumScaleFactor()
  4. 使用较大的辅助功能字号进行测试

CSS 与 Web 排版

系统 UI 字体系列:

font-family: system-ui; /* SF Pro */
font-family: ui-rounded; /* SF Pro Rounded */
font-family: ui-serif; /* New York */
font-family: ui-monospace; /* SF Mono */

旧版:

font-family: -apple-system; /* deprecated, use system-ui */

代码示例

强调的大标题(SwiftUI)
Text("Recipe Editor")
    .font(.largeTitle.bold()) // Emphasized variant
自定义字体 + 动态字体(UIKit)
let customFont = UIFont(name: "Avenir-Medium", size: 17)!
let metrics = UIFontMetrics(forTextStyle: .body)
label.font = metrics.scaledFont(for: customFont)
label.adjustsFontForContentSizeCategory = true
圆角设计(UIKit)
let descriptor = UIFontDescriptor
    .preferredFontDescriptor(withTextStyle: .largeTitle)
    .withDesign(.rounded)!
let font = UIFont(descriptor: descriptor, size: 0)
圆角设计(SwiftUI)
Text("Today")
    .font(.largeTitle.bold())
    .fontDesign(.rounded)
ScaledMetric(SwiftUI)
struct RecipeView: View {
    @ScaledMetric(relativeTo: .body) var padding: CGFloat = 20

    var body: some View {
        Text("Recipe")
            .padding(padding) // Scales with Dynamic Type
    }
}

资源

WWDC:2020-10175、2022-110381、2023-10058

文档:/uikit/uifontdescriptor、/uikit/uifontmetrics、/swiftui/font

评论

支持 Markdown