说到 SF Symbols 不知道大家有什么印象?
我们可能觉得 SF Symbols 只是一个系统内置的图标库而已
因为UI方面会提供我们开发需要的各种图标和符号,所以可能也没怎么接触它,但是它其实无处不在,从iPhone到Mac再到Apple Watch等,现在可以说你在使用苹果设备时无时无刻都能看到SF Symbols
比如这个控制中心的页面,可以说从上到下全是SF Symbols
从飞行模式,到音乐的切换按钮,再到亮度调节
甚至手电筒图标的变化
甚至到收藏,切换英语时的图标变化动画等等都是SF的使用

问题
让我们先从一个我以前遇到的问题和怎么用SF Symbol解决开始
假如我们要做一个游戏的图鉴,其中一个武器有两个版本,区分了是否受到污染


而游戏中用一个类似✨sparkles的标志,来表示武器没有被污染

我想要在app的图鉴列表里模仿这种效果,那么一般怎么才能在SwiftUI做到?在其他技术里呢?

可能最先想到的是直接在文字旁加一个视图
一个HStack就解决了
HStack(spacing: 5) {
**Text(guideItem.name)**
.strokeText(color: colorScheme == .dark ? Color(hex: "76b6ff").opacity(0.3) : .clear, lineWidth: 0.1)
.shadow(color: colorScheme == .dark ? Color(hex: "A5CBF6").opacity(0.5) : .clear, radius: 4)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .title3)
.foregroundColor(self.viewModel.getStatus(for: guideItem.id) ? Color.blue : Color.primary)
if guideItem.special != nil {
**Image("special")**
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
}
}

这样不是不行,但在多行文字的时候很难看,并没有在文字的末尾跟随着换行
而且由于图片的高度和文字不一致导致不同武器文字之间的基线也对不齐
另外这个图片本身的颜色变化也和武器拥有的状态不匹配,我们可能又需要额外的对不同状态使用不同的图片
接下来我们很自然就可能会想到把文字和图片组合
比如我们可以把图像转换为Text视图再和前面的文字Text视图组合
Text(LocalizedStringKey(guideItem.name), tableName: "default")
+
**Text(Image("special"))**
这样可以并排并且换行了

但是,这样做就会导致新的问题,首先是我们需要控制这个图片的宽高,并且由于要支持不同平台的设备(比如iPad)还需要设置不同的宽高,也许我们可以读取字号再计算,但这样很麻烦
同时之前提到的根据武器拥有状态不同而要进行对应的图片颜色变化的问题依然需要解决
而更重要的问题是这样做在需要换行的时候,组合视图总是倾向于把图片放到第二行
虽然有点吹毛求疵,但目的主要是介绍SF Symbols 的特性,好像有一点萝卜坑的感觉,但我们继续解决吧
我们按照游戏内的风格制作了一个自定义的SF Symbols,具体怎么制作后面再介绍

总之我们有了这个符号,就可以开始在项目里使用了,由于SF Symbols拥有和文字一样的特性,可以方便的和文字一起使用,放入项目后直接Image("special")
就可以用这个符号了,简单和文字组合就好了
HStack(spacing: 0) {
Text(guideItem.name)
+ Text(guideItem.special != nil ? "\u{00A0}" : "") // 和符号之间的空格
+ Text(guideItem.special != nil ? **Image("special")** : Image("transparentPlaceholder"))
}
.strokeText(color: colorScheme == .dark ? Color(hex: "76b6ff").opacity(0.3) : .clear, lineWidth: 0.1)
.shadow(color: colorScheme == .dark ? Color(hex: "A5CBF6").opacity(0.5) : .clear, radius: 4)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .title3)
.foregroundColor(self.viewModel.getStatus(for: guideItem.id) ? Color.blue : Color.primary)

我们可以看到第一行即便加入了符号,这个武器的名字的基线和其他武器名也在一条线上
而第二行在文字换行后符号也跟着换行了
当然由于SF Symbols的“文字”的特性,我们也无需额外的处理,也能让它直接和文字一起在不同的拥有状态下变色了
甚至如果有老年用户需要把字号调到无比巨大也没问题 (但老年用户应该不会玩游戏…吧?)

希望通过上面这个例子可以改变我们以前可能觉得SF Symbols就是一堆系统内置的图片的想法
接下来让我正式开始介绍它有哪些特性以及怎么制作自定义的SF Symbols吧
SF Symbols 的特性
SF 符号 (SF Symbols)提供了数千个风格一致且可配置性高的符号,这些符号可与 San Francisco 系统字体无缝整合且可自动适配所有粗细和字号的文本。可以在显示界面图标的任何地方,例如导航栏、工具栏、标签页栏、上下文菜单和文本中,使用符号来表达物体或概念。
SF Symbols 设计上考虑了排版,包含不同的权重、尺度、轮廓和填充变体、封装形状和对齐等特性。
从2019的iOS13发布起来已经进行了很多改变,下面大概是按照时间线介绍的,所以越后面的特性对系统版本的要求越高
它很重要的一个特性就是可以被当做文字来处理,SF Symbols 是一套与文本协调的矢量符号库,符号可以缩放并与文本的粗细协调,支持自定义。
比如放到文字里,它可以很好的与文本协调显示

一条水平线
在根据字体粗细大小变化时也不是简单的放大,描边而是有对应的变化和处理

不同粗细表现都很好

所以无论字体大小如何都能很好的配合
不需要我们手动进行图片大小的计算
它还会自动对齐布局

互相对齐
与文字对齐
比如我们在做这样的一个列表时,我们希望无论怎么换行,图片始终和第一行对齐


在UIkit中也许我们会设定一些约束,而在SwiftUI中可能用Spacer()之类的东西把它顶到上面去
而SF就很容易了,我们可以直接在IB里为它设置baselineOffsetFromBottom
让它和第一行文字的基线对齐

在在 SwiftUI 中,可以这样做

sf自带的基线
不过如果是普通图片,UIKit中我们还是可以手动计算一下图片基线相关的值并将这个值传递给 withBaselineOffsetFromBottom
方法来创建一个新的 UIImage
实例来做到
而在SwiftUI里也可以用类似的方式计算并和基线对齐

需要手动指定
所以通过这种方式,即使不是SF符号也能解决这个对齐问题
比如我们要把符号换成真是的头像,就可以用上面的方式做到

与文字混合使用
所以SF Symbols的这种“文字”的特性让它可以很好的插入文本里

UIKit

要改变颜色也很轻松,不需要格式化作为模板什么的了

UIKit

要加颜色也很简单
通过配置和动态类型支持,符号可以自动适应不同的环境和内容大小
本地化
有很多本地化的字符,也会自动适应,无需在代码中指定本地化变体

渲染模式
一般我们可能以为SF Symbols只支持单色,或者像文字那样被赋予颜色,但其实一个SF Symbols里也可以有多种颜色存在
SF 符号 3 及更高版本提供了四种渲染模式:单色、分层、调色盘和多色
比如在天气的组件里,就使用了多色的模式下的SF Symbols,这是预设的颜色,我们只需要指定他为Multicolor模式就好了

渲染模式有多重要呢?适当的颜色和层次选择可以给App带来很不一样的观感


接下来来介绍一下SF的四种渲染模式

单色渲染模式(Monochrome)

将一种颜色应用到符号中的所有层。符号中的路径以你指定的颜色渲染或者作为颜色填充路径中的透明形状
反映 SF Symbols 的排版本质,颜色中性,适用于追求视觉统一性的场景(不会让某一个图标特别显眼)
比如在这个长按的菜单里,我们不会想看到上面那样五颜六色的符号,而是和文字颜色统一的符号
当统一性是首要考虑因素时,单色模式是理想选择
另外它也支持不同的透明度设置


我们知道同一个系统色会在不同的模式下显示不同,SF同样也是这样
分层渲染模式(Hierarchical)

将一种颜色应用到符号中的所有层,颜色的不透明度因每一层的分层级别而异
同样和单色一样是一个色调,但是会使用不同不透明度创建视觉层次,让单一色调带动整体美感


这种效果的实现是因为在设计SF是就为它区分了不同的层次

Tertiary只在少数情况下需要,比如下雨
当我们在分层模式下为SF设置颜色时,这个颜色会作为基色自动套用在不同层次


使用方式

UIKit中使用配置来统一给图标设置
这么做有什么用呢?
可以用于强化界面中符号的可读性和辨识度。比如多个图标并排的情况下,重点就很重要了 这样可以减少视觉干扰(通过调整不透明度而非移除元素,减少视觉噪音)

调色板渲染模式(Palette)

将两种或更多种颜色应用到符号,每层使用一种颜色。为定义三个分层级别的符号仅指定两种颜色意味着第二层和第三层使用相同颜色
这个模式呢就比较自由了,之前提到SF本身是被分配了不同的层次的
在这个模式下我们可以手动的独立控制各个颜色


为什么要这么做呢?因为颜色可以标记功能组或区分不同功能的符号,你想要指定什么品牌色或者主题色的情况下也可以使用它
另外这种模式下如果不指定第三层的颜色那么他会继承第二层的颜色

-
控制符号各层的多种颜色,最多三种,支持自定义和材料样式。
-
甚至可以指定次要的颜色为模糊或者材料
-
可以。。但很少用的渐变覆盖

Image(systemName: "person.3.sequence.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(
**.linearGradient**(colors: [.red, .black], startPoint: .top, endPoint: .bottomTrailing),
.linearGradient(colors: [.green, .black], startPoint: .top, endPoint: .bottomTrailing),
.linearGradient(colors: [.blue, .black], startPoint: .top, endPoint: .bottomTrailing)
)
.font(.system(size: 144))
调色盘模式允许你对SF符号进行各种复杂而准确的颜色指定
多色渲染模式(Multicolor)

为部分符号应用有内涵的颜色以增强含义。例如,leaf
符号使用绿色反映现实世界中树叶的外观,而 trash.slash
符号使用红色提示数据丢失。部分多色符号包括可接收其他颜色的层
这种显示符号的固有或原生颜色,反映物理世界中的对象或将颜色与概念相关联。
比如叶子应该是绿色的,硬盘的添加标注应该是绿色,移除标志应该是红色的,这些都是预设的


-
支持全彩、部分彩和无多彩信息的单色渲染。
比如只有全上色,多层,或者没有色彩时使用单色
猜猜crt显示器蓝屏的图标叫什么?

另外符号现在具有首选渲染模式,可自动选择最能突出每个符号独特特性的渲染模式(根据符号的性质提供自动的首选渲染模式),渲染模式仍可以明确指定以确保统一外观
符号的多变
符号变体
说完几种渲染模式后,我们可以看到有很多类似的SF符号,它们很多时候都是一个符号的变体
同一个SF符号有多种变体以适应不同使用场景,比如简单的house就可以有很多种不同类型的变体来适配各种使用场合

自动适应
而我们在不同的地方使用[Label](https://developer.apple.com/documentation/swiftui/label)
视图来结合标题和图标,它也会自动根据场景来决定显示的方式,并自动获得辅助功能支持 比如你如果放到下方的页面切换的栏里,它就会仔细选择fill的变体

SwiftUI会根据情况自动决定怎么进行显示(你也可以进行指定)
-
自定义符号通过改变初始化器在
Image
和Label
中使用使用自定义符号的时候
-
符号在
Text
中通过字符串插值嵌入,与文本一起重排
让我们再详细一点了解它的变体,比如在列表时我们默认会使用没有填充的版本

但也可以自己指定来获得fill的版本

修改符号的表现
-
之前说过可以通过
foregroundStyle
修改符号颜色 -
font
修改符号大小,文本样式会随动态类型改变 (可以看到符号大小和文字大小一起改变) -
另外SF还有三种缩放来应对不同的使用场景(并不是单纯的等比放大,而是设计时就有考虑)
imageScale
修改符号相对于文本的比例大小。(不改变文字的大小)
可变颜色特性(Variable Color)
这是一个比较新的特性,所以放到后面。
无论使用何种渲染模式,SF 符号 4 都可通过可变颜色这种方式来表现可随时间而变化的特征,例如容量或强度。为了在视觉上体现这种变化,可变颜色会根据值到达 0%~100% 的不同阈值将颜色应用到符号的不同层
我想最能感受到不同的地方应该是在分享屏幕时,无论是Mac还是iPhone,新版本的系统中分享屏幕时符号都会闪烁了。这就是可变颜色的使用

之前说过SF本身有分层,而现在这种分层也可以被用来制作变化的符号了

比如Wi-Fi,信号改变,加载等等

可变颜色允许突出表示不同强度级别或时间序列的路径
比如这个音量符号,会随着百分比的改变调整外观


通过百分比值改变符号的外观,反映随时间变化的值

会均匀分配,对于不能平分的会四舍五入,然后从下一个百分百开始进入下个阶段
变色适用于所有渲染模式,没有关于符号受影响部分数量的规则,也就是说可以加很多很多个变化的层,比如加载图标那样
动画
在2022年可变色彩的基础上,今年的WWDC上推出了SF动画的预设 (SF 符号 5)
- 提供多种可配置的动画预设,适用于所有规模和渲染模式的符号。
- 动画通过分层进行,每层都能一次动画,使得符号的编排清晰准确。
- 也可以选择让所有层同时动画。
- 动画空间是指符号在应用运动时创造深度感的维度。不同的平面有不同的视觉效果,比如中间平面是参考点,前平面使符号看起来更大,后平面则相反。
动画库的不同预设
-
出现(Appear):符号渐渐进入视野时使用。
-
消失(Disappear):符号从界面中移除时使用。
-
弹跳(Bounce):模拟物体弹跳的动态效果,反馈交互成功或动作发生。
比如进行书签标记或者反馈亮度的调节 -
缩放(Scale):通过改变符号大小提供视觉反馈,强调符号重要性或确认交互。比如变小时表示被按下了,或者比如在iPad用鼠标操作时图像的变大表示被关注点
另外缩放还具有状态,只有取消时才会恢复弹跳和缩放的主要区别在于弹跳表示一个动作已经发生或者需要发生,而缩放可以在选择一个东西时提供焦点或反馈
-
脉冲(Pulse):通过改变不透明度传达持续活动的状态,强化符号表达的概念。
比如新系统的随航就跟着变化了 -
可变颜色**(Variable Color)**:之前提到的可变颜色,今年也包含在动画库中了,现在有累积(Cumulative)和迭代(Iterative)两种表现形式,更加灵活了
比如开启Wi-Fi时用累积,寻找Wi-Fi时用迭代
-
替换(Replace):符号状态变化时,一个符号替换另一个,传达符号状态变化。
至于变成什么符号可以自己指定,也可以分层或整个变化比如音乐播放按钮就可以使用它,或者在选择文字时键盘的变化
接下来的发展?
SF Symbols推出以来从数量增加(现在已经有5000+个了),到多种渲染模式提供更加多彩的表达再到可变颜色和预设动画,越来越灵活了,让我们可以方便的对它调用和修改出不同的表现 用在不同的场景
在 iOS 的17.2 Beta 3里,这种灵动的趋势被进一步发展了
和刚刚介绍的简单的那些动画效果不一样,这一版beta里闹钟符号的动画更加灵动了甚至有点动态模糊的效果,和真实的闹钟非常相似
而系统里切换歌曲的SF也有着不一样的动画表现
也许明年WWDC我们就能看到更多符号拥有更复杂的预设动画,甚至我们也可能自定义出如此灵动动画的SF了
总结
现在回过头来看在苹果各种不同系统和各种应用上我们都能看到SF的大量使用,为用户提供了一种一致性和多变而灵活的体验,更重要的是作为开发者,我们也可以通过创建自己的SF符号使用各种特性来让我们开发的App也能轻松获得更好的符号表现



最后再吐槽一下,这么多图片后面发乐享时会累死吧?😂
Demo
一个简单的SwiftUI的SF符号使用展示
展示了各种渲染模式和文字组合、Label的使用等,其中调色盘模式的SF
符号颜色是随机的

资料
如果想进一步深入,以下是官方相关的资料
WWDC:
- Introducing SF Symbols
- SF Symbols 2
- What’s new in SF Symbols
- SF Symbols in SwiftUI
- SF Symbols in UIKit and AppKit
- Create custom symbols
- What's new in SF Symbols 4
- Adopt Variable Color in SF Symbols
- What’s new in SF Symbols 5
- Create animated symbols
文档: