The details of UI typography
Design 入门 22m

UI 排版的细节之道

The details of UI typography

2020年6月23日

在 Apple 官方观看视频

一句话判断

排版不只是”选个字体然后调大小”——这场 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 提到了 CTFramesetterCTTypesetter 等 Core Text API,适合需要极致排版控制的场景
WWDC 2020