WWDC Quick Look 💓 By SwiftGGTeam
The details of UI typography

The details of UI typography

观看原视频

Highlight

Apple 在 WWDC 2020 将 UI 排版落到一组可执行 API:SF Pro 迁移到 variable fonts,系统自动同步 optical size;UIKit、AppKit 和 SwiftUI 补齐 text style、leading 与 system design 入口,UIKit 和 SwiftUI 继续完善自定义字体的 Dynamic Type 支持。

核心内容

00:37)很多 App 的排版问题常常等到真实设备上才暴露:同一段文字到了小字号变糊,按钮标题差几个像素就截断,用户把 Dynamic Type 调大后,自定义字体的标题和内边距没有一起变化。开发者表面上只是在选字体和字号,实际要处理 optical size、tracking、leading、text style 和 accessibility 设置之间的联动。

01:47)Apple 先解释 San Francisco(SF)为什么有 SF Text 和 SF Display。20 pt 以下的小字号需要更宽的字距和更稳的细节,20 pt 以上的大字号可以保留更多视觉性。2020 年的变化是 SF Pro 开始转向 variable fonts(可变字体),字体内部用 optical size axis 表达尺寸变化,系统在代码里自动把 point size 和 optical size 对齐。(06:31

09:22)这件事直接影响 UI 布局。tracking 不是 kerning;tracking 是整段文本的字距调整,kerning 是字偶之间的微调。标题快要被截断时,用 kerning API 硬挤文本会让 ligature 保持原样,节奏会坏掉。使用 tracking API 或允许系统自动 tightening,系统可以按字号使用合适的 tracking table,并在必要时拆开 ligature。

16:11)后半段把这些原则接到 UIKit、AppKit 和 SwiftUI。Text styles(文本样式)把字号、字重和行高组合成系统层级;Dynamic Type(动态字体)让这些组合跟随用户偏好缩放。自定义字体也不需要放弃 accessibility:UIKit 用 UIFontMetrics,SwiftUI 在 iOS 14 里让 .custom 默认按 body 缩放,并用 relativeTo 指定目标文本样式。(25:10

详细内容

Tracking:用正确的 API 处理字距和截断

12:19)如果要手动调整整段文字的字距,UIKit 侧使用 Core Text 的 tracking attribute,SwiftUI 侧使用 .tracking()。session 特别提醒,不要把这种需求误当成 kerning。

// UIKit
label.attributedText =
    NSAttributedString(string: "hamburgefonstiv",
        attributes: [kCTTrackingAttributeName as NSAttributedString.Key: -0.5])

// SwiftUI
Text("hamburgefonstiv").tracking(-0.5)

关键点:

  • kCTTrackingAttributeName 表达的是整段文字的 tracking,不是某两个字符之间的 kerning。
  • -0.5 会收紧字符间距;这个值应按字号验证,不能拿小字号的值直接套到大标题。
  • SwiftUI 的 .tracking(-0.5) 是同一类语义,适合少量需要精细调整的文本。

12:45)更常见的截断场景应该交给系统。allowsDefaultTighteningForTruncation.allowsTightening(true) 会使用系统字体中的 tight tracking table,在可读范围内尝试让字符串放下。

// UIKit: UILabel
label.allowsDefaultTighteningForTruncation = true

// AppKit: NSTextField
textField.allowsDefaultTighteningForTruncation = true

// SwiftUI
Text("hamburgefonstiv").allowsTightening(true)

关键点:

  • UIKit、AppKit 和 SwiftUI 都有对应入口,适合按钮、标签、导航标题这类空间紧张的文本。
  • 系统会控制 tightening 的范围;超出合理范围时,截断仍然是更好的结果。
  • 这条路径会得到 size-specific tracking,开发者不用自己维护 tracking table。

Text styles:从系统层级拿到强调和行高变体

17:45)Text style 不只是字号。要做 emphasized title1,可以先拿 preferredFontDescriptor(withTextStyle:),再叠加 bold symbolic trait。实际映射到 medium、semibold、bold 还是 heavy,由系统按 text style 决定。

// Getting emphasized text styles

let label = UILabel()
label.text = "Ready. Set. Code."

if let descriptor = UIFontDescriptor
    .preferredFontDescriptor(withTextStyle: .title1)
    .withSymbolicTraits(.traitBold) {
    // 28 pt Bold on iOS
    label.font = .init(descriptor: descriptor, size: 0)
}

关键点:

  • preferredFontDescriptor(withTextStyle: .title1) 保留了 title1 的系统尺寸和 Dynamic Type 行为。
  • .withSymbolicTraits(.traitBold) 请求强调变体,不等于固定指定某个字重数值。
  • size: 0 表示使用 descriptor 里的系统尺寸,不手动覆盖字号。

20:03)行高也可以从 text style 派生。tight leading 适合信息密度更高的界面,loose leading 适合长段阅读;session 中的例子显示 body 文本从 22 pt 行高变到 20 pt 或 24 pt。

// Getting tight/loose leading variant

// AppKit
let descriptor = NSFontDescriptor.preferredFontDescriptor(forTextStyle: .headline)
    .withSymbolicTraits(.tightLeading) // Use .looseLeading for loose leading font
let tightLeadingFont = NSFont(descriptor: descriptor, size: 0) // 14 pt line height

// UIKit/Catalyst
if let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1)
    .withSymbolicTraits(.traitTightLeading) { // Use .traitLooseLeading for loose leading
    let tightLeadingFont = UIFont(descriptor: descriptor, size: 0) // 36 pt line height
}

// SwiftUI
// Use .loose for loose leading font
let tightLeadingFootnoteFont = Font.footnote.leading(.tight) // 16 pt line height on iOS

关键点:

  • AppKit 使用 .tightLeading.looseLeading,UIKit/Catalyst 使用 .traitTightLeading.traitLooseLeading
  • SwiftUI 用 .leading(.tight).leading(.loose) 接到系统 text style。
  • tight leading 不适合所有长文本;session 在 19:43 明确展示 body 段落过紧时阅读感会变差。

System designs:保留 text style,只替换字体气质

20:56)SF Pro Rounded、New York 和 SF Mono 可以跟 text style 一起工作。对 UIKit 来说,做法是从现有 text style descriptor 出发,再调用 .withDesign(.rounded);这样字体设计变了,字号、字重和行高仍沿用系统样式。

// Access rounded system font design
import UIKit

let label = UILabel()
label.text = "Today"

if let descriptor = UIFontDescriptor
    .preferredFontDescriptor(withTextStyle: .largeTitle)
    .withSymbolicTraits(.traitBold)?
    .withDesign(.rounded) {
    // SF Pro Rounded Bold
    label.font = UIFont(descriptor: descriptor, size: 0)
}

关键点:

  • .preferredFontDescriptor(withTextStyle: .largeTitle) 先确定系统层级。
  • .withSymbolicTraits(.traitBold) 得到 emphasized large title。
  • .withDesign(.rounded) 把设计切到 SF Pro Rounded,但保留 large title 的尺寸、字重语义和行高。

21:08)同一能力在 AppKit、UIKit/Catalyst 和 SwiftUI 里都有入口。SwiftUI 要在构造 font 时传入 design,而不是把已有 font 再转换一次。

// Access system font designs

// Use .serif for New York, .monospaced for SF Mono

// AppKit
let descriptor = NSFontDescriptor.preferredFontDescriptor(forTextStyle: .body)
    .withDesign(.rounded)
let roundedBodyFont = NSFont(descriptor: descriptor, size: 0) // SF Pro Rounded

// UIKit/Catalyst
if let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
    .withDesign(.rounded) {
    let roundedBodyFont = UIFont(descriptor: descriptor, size: 0) // SF Pro Rounded
}

// SwiftUI
let roundedBodyFont = Font.system(.body, design: .rounded) // SF Pro Rounded

关键点:

  • .serif 对应 New York,.monospaced 对应 SF Mono,.rounded 对应 SF Pro Rounded。
  • UIKit/Catalyst 和 AppKit 都从 font descriptor 进入。
  • SwiftUI 的 Font.system(.body, design: .rounded) 同时声明 text style 和 design。

Dynamic Type:让自定义字体和布局一起缩放

25:05)UIKit 从 iOS 11 开始提供 UIFontMetrics。它把某个 text style 的 Dynamic Type 缩放曲线应用到任意自定义字体,也能缩放布局常量。

// Support Dynamic Type with custom font in UIKit

if let customFont = UIFont(name: "Charter-Roman", size: 17) {
    let bodyMetrics = UIFontMetrics(forTextStyle: .body)

    // Charter-Roman scaled relative to body text style
    // in different content size categories.
    let customFontScaledLikeBody = bodyMetrics.scaledFont(for: customFont)
    label.font = customFontScaledLikeBody
    label.adjustsFontForContentSizeCategory = true

    // Scaling constant 10 relative to body text style.
    let scaledValue = bodyMetrics.scaledValue(for: 10)
}

关键点:

  • UIFontMetrics(forTextStyle: .body) 选择 body 的缩放行为,不只是拿一个固定倍率。
  • scaledFont(for:)Charter-Roman 跟随用户的文字大小设置。
  • adjustsFontForContentSizeCategory = true 让 label 在设置变化时继续响应。
  • scaledValue(for:) 适合把间距、padding 这类布局常量同步放大。

26:25)SwiftUI 在 iOS 14 改进了自定义字体缩放。.custom(_:size:relativeTo:) 可以指定按哪个 text style 缩放;省略 relativeTo 时,默认按 body 缩放。@ScaledMetric 则把布局值接到同一套文字大小设置。

struct ContentView: View {
    let prose = "Apple provides two type families you can use in your iOS apps. San Francisco (SF). San Francisco is a sans serif type family that includes SF Pro, SF Pro Rounded, SF Mono, SF Compact, and SF Compact Rounded."
    @ScaledMetric(relativeTo: .body) var padding: CGFloat = 20

    var body: some View {
        VStack {
            Text("Typography")
                .font(.custom("Avenir-Medium", size: 34, relativeTo: .title))
            Text(prose)
                .font(.custom("Charter-Roman", size: 17))
                .padding(padding)
        }
    }
}

关键点:

  • 标题用 Avenir-Medium,但通过 relativeTo: .title 使用 title 的缩放曲线。
  • 正文用 Charter-Roman,省略 relativeTo 后按 body 缩放。
  • @ScaledMetric(relativeTo: .body) 让 padding 跟正文尺寸一起变化,避免大字号时文本贴边。

核心启发

  • 品牌字体的阅读页:做一个支持 Dynamic Type 的文章或说明页,让品牌字体随用户文字大小变化。为什么值得做:session 明确把 custom font 和 accessibility 连接起来。怎么开始:UIKit 用 UIFontMetrics(forTextStyle:) 包装自定义字体,SwiftUI 用 .custom(_:size:relativeTo:)@ScaledMetric 同步字体与 padding。

  • 截断敏感的按钮和导航标题:为短按钮、标签和导航栏标题开启系统 tightening,而不是手写 kerning。为什么值得做:系统会使用 tight tracking table,并在可读范围内调整。怎么开始:UIKit/AppKit 设置 allowsDefaultTighteningForTruncation,SwiftUI 设置 .allowsTightening(true),再用最长本地化文案测试。

  • 小屏信息密度模式:给 watchOS、widget 或紧凑卡片准备 tight leading 版本。为什么值得做:session 用 watchOS 和 Fitness 的例子说明 tight leading 可以提升单位屏幕的信息量。怎么开始:优先使用系统 text style,再在 SwiftUI 中试 .leading(.tight),长段正文保留默认或 loose leading。

  • 有品牌感但不破坏层级的系统字体方案:在提醒、记账、阅读类 App 里用 rounded、serif 或 monospaced design 表达不同内容气质。为什么值得做:.withDesignFont.system(_:design:) 会保留 text style 的字号、字重和行高。怎么开始:标题尝试 .rounded,正文阅读尝试 .serif,金额或编号尝试 .monospaced

  • 排版回归检查清单:把 optical size、tracking、leading 和 Dynamic Type 放进 UI QA。为什么值得做:session 的核心问题都发生在真实尺寸、真实语言和真实用户设置下。怎么开始:每个关键界面至少检查小字号、大标题、最大辅助功能字号、长本地化字符串和阿拉伯语等高行高语言。

关联 Session

  • SF Symbols 2 — SF Symbols 与 San Francisco 字体共享视觉系统,适合继续看 symbol、文本样式和多框架 API 的配合方式。
  • Make your app visually accessible — 从可读文本、视觉设置和 accessibility 角度补齐排版在真实用户设置下的检查项。
  • Design great widgets — widget 的信息必须快速扫读,里面的 typography、尺寸和布局取舍与本 session 直接相邻。
  • Adopt the new look of macOS — macOS Big Sur 的视觉层级和 AppKit 更新提供了桌面端 text style 落地背景。
  • What’s new in watchOS design — watchOS 小屏界面对行高、信息密度和直接操作的取舍,是 tight leading 场景的延伸。

评论

GitHub Issues · utterances