SwiftUI 中的 SF Symbols
SF Symbols in SwiftUI
2021年6月11日
一句话判断
如果你还在用 UIImage + UIImageView 加载 SF Symbols,停下来——SwiftUI 的 Symbol API(特别是 Rendering Modes)用三行代码就能实现以前需要 Core Image 滤镜才能做的效果。
这场 Session 讲了什么
这个 Session 专门讲解如何在 SwiftUI 中使用 SF Symbols,是「Explore the SF Symbols 3 app」(Session 10288)的 SwiftUI 实战篇。核心内容围绕 iOS 15 中新增的 Symbol API 展开:symbolRenderingMode、foregroundStyle 的多值传参、以及 symbolVariant 修饰符。
在 iOS 14 中,SwiftUI 的 SF Symbols 支持比较基础——只能用 .renderingMode(.original) 或 .renderingMode(.template),对颜色的控制粒度很粗。iOS 15 彻底改变了这个局面:四种渲染模式(Monochrome、Multicolor、Hierarchical、Palette)都有对应的 SwiftUI API,foregroundStyle 可以接受多个颜色参数(分别对应 Symbol 的不同图层),symbolVariant 让你动态切换 Symbol 的变体(比如从 outline 切换到 fill)。
Session 还讲解了 SF Symbols 与 SwiftUI 主题系统的集成:当用户切换 Light/Dark 模式或 Accent Color 时,使用 foregroundStyle(.tint) 的 Symbol 会自动跟随主题变化。
值得深挖的点
foregroundStyle 的多值传参机制。 当你使用 Palette 渲染模式时,foregroundStyle 需要为每个图层指定颜色。SwiftUI 的类型系统巧妙地支持了这个需求——foregroundStyle 接受 variadic 参数(_ styles: S),你可以传一个、两个或三个颜色。第一个颜色对应最上层(前景),第二个对应中间层,第三个对应最下层。比如一个文件夹图标的 Palette 模式:.foregroundStyle(.white, .blue) 会把标签页渲染成白色、文件夹本体渲染成蓝色。这个设计比 UIKit 的 withTintColor(_:renderingMode:) 灵活得多。
symbolVariant 的组合能力。 symbolVariant 修饰符让你可以动态选择 Symbol 的变体,而不需要在代码中硬编码变体名称。比如 .symbolVariant(.fill) 会把 star 变成 star.fill,.symbolVariant(.circle.fill) 会把 star 变成 star.circle.fill。这在与按钮状态绑定时特别有用——选中状态用 .fill 变体,未选中状态用默认变体,不需要写 if-else 来切换不同的 systemName。
代码片段
import SwiftUI
// 四种渲染模式的 SwiftUI 用法
struct SymbolModesView: View {
var body: some View {
VStack(spacing: 20) {
// Monochrome:单色,用 foregroundStyle 着色
Image(systemName: "bell.badge.fill")
.symbolRenderingMode(.monochrome)
.foregroundStyle(.blue)
// Hierarchical:自动分层,前景不透明,背景半透明
Image(systemName: "bell.badge.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.red)
// Palette:手动指定每层颜色
Image(systemName: "bell.badge.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
// Multicolor:保留系统自带的多色
Image(systemName: "bell.badge.fill")
.symbolRenderingMode(.multicolor)
}
.font(.largeTitle)
}
}
// symbolVariant 与按钮状态绑定
struct ToggleButton: View {
@State private var isFavorite = false
var body: some View {
Button {
isFavorite.toggle()
} label: {
Image(systemName: "heart")
.symbolVariant(isFavorite ? .fill : .none)
.foregroundStyle(isFavorite ? .red : .gray)
.font(.title)
.animation(.easeInOut(duration: 0.2), value: isFavorite)
}
}
}
// 配合 Label 和 List 使用 SF Symbols
struct MenuListView: View {
var body: some View {
List {
Label("收件箱", systemImage: "tray.and.arrow.down")
Label("已发送", systemImage: "paperplane")
Label("草稿", systemImage: "doc.text")
Label("垃圾箱", systemImage: "trash")
}
// 在 toolbar 中使用 Palette 模式
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
compose()
} label: {
Image(systemName: "square.and.pencil")
.symbolRenderingMode(.palette)
.foregroundStyle(.blue, .secondary)
}
}
}
}
}
最佳实践
- 优先使用
symbolVariant而不是手动拼接 systemName 字符串——前者有编译时检查,后者拼错了只有运行时才发现 - 在 Light/Dark 模式下测试你的 Symbol 配色——
foregroundStyle(.tint)会跟随主题,但硬编码的颜色不会 - 对于需要动画的 Symbol 切换(比如 heart → heart.fill),使用
.animation(.spring())而不是.animation(.default),弹性动画在图标变形时更自然 - 用 SF Symbols 3 应用的「Copy SF Symbol Name」功能确保你用的 systemName 拼写正确
还有什么值得关注
- SwiftUI 的
Label视图是使用 SF Symbols 的最佳方式——它会自动处理 VoiceOver 标签和布局 contentTransition(.symbolEffect)在后续版本中为 Symbol 添加了内置的过渡动画- 自定义 Symbol 的 SVG 模板可以从 SF Symbols 3 应用导出,直接拖进 Asset Catalog 使用