UI 排版的细节之道
The details of UI typography
2020年6月23日
一句话判断
排版不只是”选个字体然后调大小”——这场 Session 用大量实际案例告诉你,好的 UI 排版是像素级的精雕细琢,而 Apple 的文本系统提供了你可能从没用过的精细控制能力。
这场 Session 讲了什么
这场 Session 是一次关于 UI 排版的深度教学。从字体的度量系统(metrics)到文本布局的微调技巧,内容覆盖了排版设计的方方面面。Session 首先解释了字体的基础度量概念:ascent、descent、cap height、x-height、leading 和 kerning,这些看似学术的概念直接决定了你的 UI 中文字看起来是否”舒服”。
Session 重点讨论了 SF Pro 字体的设计特性和使用技巧。SF(San Francisco)作为 Apple 的系统字体,内置了多种光学尺寸(optical size):Text(小尺寸阅读)、Display(大尺寸标题)和 Monospaced(等宽数字和代码)。系统会根据字号自动选择合适的光学尺寸,但开发者也可以手动控制。此外,SF 的圆角版本 SF Rounded 和比例版 SF Serif 为不同场景提供了更多选择。
在布局层面,Session 详细讲解了行距(line spacing)、段间距、基线对齐(baseline alignment)和动态类型(Dynamic Type)的最佳实践。特别值得注意的是 Dynamic Type 的适配——你的 UI 需要正确响应用户的字体大小设置,从最小的辅助功能尺寸到最大的超大尺寸,布局不能崩。
值得深挖的点
SF 字体的光学尺寸自动切换
SF 字体内置了光学尺寸机制:当字号小于 20pt 时使用 Text 版本(字间距更大、笔画更粗以保持可读性),大于 20pt 时自动切换到 Display 版本(更精致、更紧凑)。这个切换是连续的,不是二元的。理解这一点很重要——如果你手动设置了 UIFont.systemFont(ofSize: 19) 和 UIFont.systemFont(ofSize: 21),它们的字形轮廓会有微妙但可感知的差异。
Dynamic Type 的正确适配策略
Dynamic Type 不仅仅是字体变大变小。每个 Text Style(如 .title1、.body、.caption1)在不同 Dynamic Type 级别下的缩放曲线不同——标题的缩放幅度小,正文的缩放幅度大。这意味着你不能简单地把所有文字按同一比例缩放。Session 建议使用 Auto Layout 的 intrinsic content size 和 stack view 来让布局自动适应字体变化,而不是用固定的 frame。
代码片段
import SwiftUI
// 使用系统文本样式和 Dynamic Type
struct ArticleView: View {
let article: Article
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
// 标题:使用 .largeTitle 样式
Text(article.title)
.font(.largeTitle) // 自动适配 Dynamic Type
.fontWeight(.bold)
// 副标题
Text(article.subtitle)
.font(.title3)
.foregroundColor(.secondary)
// 正文
Text(article.body)
.font(.body) // 正文使用 body 样式
.lineSpacing(6) // 适当的行距提升可读性
.tracking(0.2) // 微调字间距
// 脚注
Text(article.footer)
.font(.footnote)
.foregroundColor(.secondary)
}
.padding()
}
}
}
// UIKit 中精确控制排版
import UIKit
class TypographyLabel: UILabel {
func configure(text: String, style: UIFont.TextStyle) {
// 使用系统文本样式,自动适配 Dynamic Type
font = UIFont.preferredFont(forTextStyle: style)
adjustsFontForContentSizeCategory = true // 启用 Dynamic Type
// 精细调整段落样式
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = 1.12 // 行高倍数(比 lineSpacing 更稳定)
paragraphStyle.paragraphSpacing = 12 // 段间距
paragraphStyle.hyphenationFactor = 0.5 // 英文断字因子
// 组合属性字符串
let attributedString = NSAttributedString(
string: text,
attributes: [
.paragraphStyle: paragraphStyle,
.kern: 0.3, // 字间距微调
]
)
attributedText = attributedString
numberOfLines = 0 // 允许多行
}
}
// 等宽数字和表格对齐
import SwiftUI
struct PriceListView: View {
let items: [(name: String, price: Double)] = [
("拿铁", 28.00),
("美式", 18.50),
("卡布奇诺", 32.00),
]
var body: some View {
VStack(alignment: .leading) {
ForEach(items, id: \.name) { item in
HStack {
Text(item.name)
.font(.body)
Spacer()
// 使用等宽数字,确保小数点对齐
Text("¥\(item.price, specifier: "%.2f")")
.font(.body)
.monospacedDigit() // 关键:等宽数字
.frame(width: 80, alignment: .trailing)
}
}
}
.padding()
}
}
// 基线对齐的标签
struct TagView: View {
let tags: [String]
var body: some View {
HStack(alignment: .lastTextBaseline) { // 基线对齐
ForEach(tags, id: \.self) { tag in
Text(tag)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.1))
.cornerRadius(4)
}
}
}
}
最佳实践
- 始终使用系统文本样式(
.title、.body、.caption)而非固定字号,确保 Dynamic Type 正确工作 - 正文行距设置为字号的 1.1-1.2 倍,太紧影响可读性,太松浪费空间
- 数字列表和价格表使用
.monospacedDigit()确保数字宽度一致,方便对齐 - 测试 Dynamic Type 时至少覆盖”最小”和”最大辅助功能”两个极端
- 中英文混排时注意字间距的调整——中文字符之间不需要额外间距,但中英文之间需要留出视觉间隔
还有什么值得关注
- SF Symbols 2 新增了与文本基线对齐的能力,图标和文字的垂直居中不再是估算值
- Apple 的 Typography guidelines 页面提供了完整的度量参考表
- Session 提到了
CTFramesetter和CTTypesetter等 Core Text API,适合需要极致排版控制的场景