iOS端矢量图解决方案汇总(SVG篇)
简介
矢量图,指的是通过一系列数学描述,能够进行无损级别的变化和缩放的一种图像。相比于标量图(如JPEG等标量图压缩格式),能够在绘制时进行任意大小伸缩而不产生模糊,甚至能够实现动态着色,动画等等一系列交互。
在当今移动端设备尺寸越来越复杂,各种操作系统级别的夜间主题(或者Dark Mode)越来越提倡的场景下,如果依旧使用标量图,我们需要针对不同的屏幕大小(如2x,3x),和对应主题场景(Light/Dark),提供NxM数量级的标量图,对于App大小开销是很大的。因此,使用矢量图是一个非常有效的解决方案。这个系列文章,就是主要侧重讲解iOS端上的矢量图解决方案。
第一章是关于SVG及其相应衍生方案的解决方案,后续会有其他矢量图相关的PDF章节,Lottie等。他们各自有不同的细节场景区分和优缺点。
SVG作为目前在Web上最流行的矢量格式,在iOS端的支持可以说是一言难尽。在这里,我从各个方向上总结了截至目前已有的实现(公开的方案,企业内部实现无从得知),方便对比选择最适合自己场景的选择。
Symbol Image
Symbol Image,是Apple在WWDC 2019和iOS 13上提供的矢量图解析方案。
之所以名称叫做Symbol Image,源自于这个技术方案的实现细节,它最早诞生于SVG字体规范:OpenType-SVG。这个规范是Adobe提出的,并且得到了包括Microsoft在内的多家公司支持。Apple自己的CoreText字体框架,其实早早就在iOS 11时代内部支持了SVG类型的font table。
制作Symbol Image
Symbol Image的整体API设计,其实不像是图像,更像是一种字体(和Icon Font类似)。
对于同一个Symbol Image,它可以看作是一个SVG Path的集合。前面提到,Symbol Image基于OpenType-SVG字体,对于字体来说,我们都知道字重的概念,用来决定渲染时候的线条粗细程度。
因此Symbol Image也有9个字重:Ultralight,Thin,Light,Regular,Medium,Semibold,Bold,Heavy,Black。与此同时,Symbol Image对每一个字重,支持了3种大小,分别是Small,Medium和Large。这也就是说,一个Symbol Image最多可以有27种大小字重的样式选择。
一般来说,从头构建一个Symbol Image会非常复杂,Apple推荐的方式,是通过使用SF Symbols App,来导出一个SVG模版,再通过Sketch来进行图层编辑。
从原始的SVG数据来看,每一个Symbol Image包含的所有样式都是一个单独的Path节点,对应了图标的绘制。如果要新建一个Symbol Image,需要完全删除Path节点,重新绘制矢量路径。
1 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="3300" height="2200"> |
导入Symbol Image
导入Symbol Image的方式非常简单,你只需要将制作好的Symbol Image,向Xcode的Asset Catalog窗口拖动,就可以集成。Xcode可以会展示对应的预览效果。
另外,实际上产生的文件夹后缀为.symbolset
,这个不同于普通的Asset Image(后缀名.imageset
),也就意味着你可以同时引入一个同名的Symbol Image和普通Image。
使用Symbol Image
对于iOS 13系统提供的自带Symbol Image,UIKit提供了init(systemName:)方法来获取,对于App自行提供的Symbol Image,我们使用init(named:)方法。
注意,你可以同时包含一个Symbol Image和普通的Asset Image,共享一个Name。这样设计的好处,在WWDC上有介绍,是为了兼容iOS 12等低系统版本,在iOS 13上,Symbol Image优先级永远高于普通Asset Image,在iOS 12会自动fallback。
1 | let imageView = UIImageView() |
对于Symbol Image来说,我们可以指定在运行时需要的字重
1 | let regularSymbolImage = UIImage(named: "my.symbol.image") |
另外,我们还可以配合AttributedString使用,只要使用TextAttachment传入对应的Symbol Image即可。
1 | let textView = UITextView() |
优缺点
优点:
- iOS原生支持,工具链完善
- SwiftUI原生支持,截止目前Image能唯一使用的矢量方案(排除UIViewRepresentable)
- 支持和AttributedString无缝混合,类似Icon Font
缺点:
- iOS 13+ Only
- 通过字体属性控制大小,取决于UI场景,做到Pixel级别的拉伸会是一个问题
- 需要单独制作Symbol Image,跨平台,Web使用痛点
CoreSVG
CoreSVG是iOS 13支持Symbol Image的背后的底层SVG渲染引擎,使用C++编写。
截至目前,CoreSVG依然属于Private Framework,社区也有很多人向Apple提了反馈并建议开放出来,可能在之后的WWDC 2020我们能够得知更多的消息。
注意!以下方法均为使用了CoreSVG的Private API,可能随着操作系统变动会有改变,并且有审核风险,如果需要线上使用,请自行进行代码混淆等方案。
通过Asset Catalog使用SVG
目前Xcode不支持直接拖动SVG文件来集成到Asset Catalog,因为拖动SVG默认会当作Symbol Image处理。
但是我们可以通过一个取巧的方式来实现,Xcode支持PDF矢量图(从iOS 11与Xcode 9开始支持,PDF章会讲解)。因此,我们可以将SVG后缀改成PDF,然后拖动到Xcode中,最后再修改回SVG后缀名,并且同步.imageset/Contents.json
里面的文件名即可,如下:
当你添加好SVG图像后,可以通过Name,以和PDF矢量图一样的方式来引入和使用,如下
1 | UIImageView *imageView = [UIImageView new]; |
从运行时来看,加入Asset Catalog的SVG矢量图的UIImage,含有对应的CGSVGDocumentRef对象,并且也包含了一个标量图的缩略图,可以供缩略图或者其他系统API来调用。并且在Xcode的Interface Builder上也会有明显的SVG标识(类似PDF)
加载任意SVG数据(网络)
除了能够通过Asset Catalog添加SVG图像,通过CoreSVG,我们可以在运行时去解析网络数据下载得到的SVG数据,为此能提供更为广阔的应用场景。
1 | UIImageView *imageView = [UIImageView new]; |
渲染SVG矢量图到标量图
一些UIKit的视图,或者一些图像处理,对矢量图支持并没有考虑,或者是我们在做性能优化时,需要将矢量图光栅化得到对应的标量图。CoreSVG提供了和CoreGraphics的PDF类似的接口,允许你去绘制得到对应的标量图。
1 | CGSVGDocumentRef document; // 原始SVG Document |
SVG导出
目前,CoreSVG没有提供类似于PDF的修改元素的接口,我们只能直接对SVGDocument进行导出。或许随着未来框架的开放,会有类似于目前CoreGraphics对PDF进行编辑的高级接口。
1 | // 获取SVG Document |
优缺点
优点
- 能够支持目前已有的大量SVG,在Android和Web端复用
- Apple原生支持,稳定性有一定保证,并且随系统升级会持续优化
- 性能高,CoreSVG利用了CoreGraphics系统库和内部的SPI做矢量绘制,目前性能最好
缺点
- 目前是私有Framework,有审核和使用风险
- 可能存在一些SVG元素兼容问题,需要不断摸索
- SwiftUI不支持,需要使用UIViewRepresentable
三方SVG库
SVGKit
SVGKit是最早的iOS上开源SVG渲染方案,已经有8年之久。SVGKit内部支持两种渲染模式,一种是通过CPU渲染(CoreGraphics重绘制),一种是通过GPU渲染(CALayer树组合)。有着不同的兼容性和性能。
示例
1 | // CPU渲染 |
优点
- 支持纯Objective-C
- 如果是支持的图像,性能相对较高(1000个级别的Path可在1秒内渲染)
缺点
- 社区不再维护,大量Issue无人跟进解决
- 不遵循语义版本号,用分支发布更新,下游无法依赖
- 部分SVG特性虽然声明支持,但存在问题,如Gradient等,缺少单测
- 不支持SVG动画
Macaw
Macaw是一个矢量绘制框架,提供了非常简单的DSL语法来描述矢量路径绘制的场景。它本身不是和SVG强绑定的,但是对SVG格式提供了兼容和支持
示例
1 | let node = try! SVGParser.parse(path: "/path/to/svg") |
优点
- 目前最活跃和成熟的iOS端SVG开源框架(在GitHub上)
- 支持DSL去直接生成矢量图,修改节点等,非常强大
- 支持SVG动画(部分特性)
缺点
- 部分SVG特性特性声明不支持
- SVG性能渲染差(相对于SVGKit),依赖大量的的CPU绘制操作(非CALayer组合),可能需要结合异步绘制框架
SwiftSVG
SwiftSVG是一个专门针对SVG Path等常见特性的矢量图解析框架,他不侧重于完整的SVG/1.1规范支持,而是保证了基本的绘制实现的正确性,并且支持导出SVG的Path到UIBezierPath
示例
1 | let svgURL = URL(string: "https://openclipart.org/download/181651/manhammock.svg")! |
优点
- 性能相对MacPaw较好
- 对Path,Circle等常见元素,有着良好的兼容性和完整单测,基本上只用这些特性的SVG不存在问题
- 支持导出UIBezierPath,可以用作一些描边的交互
- 提供了便携方法,能直接读取Xcode的Data Asset,URL等
缺点
- 基本上只针对Path,Circle等元素有良好的支持,其他的Gradient,Text等均不支持
- 不支持SVG动画
VectorDrawable
VectorDrawable是Android平台上官方提供的一套矢量图解决方案,他是以一个类似SVG的XML表达形式,来描述矢量图的绘制方式。
从整体设计上看,VectorDrawable基本上是对SVG的精简和二次改造,大部分的元素在SVG中都有对应的概念,并且样式属性也一一对应。甚至,Android Studio支持直接将SVG导出成VectorDrawable文件并直接集成。
在iOS上平台上,Uber内部开源了一套自己在用的VectorDrawable实现:Cyborg,通过利用CoreGraphics和CoreAnimation来渲染VectorDrawable文件。
使用VectorDrawable渲染
VectorDrawable提供了一个专门用于矢量图的View,并且能够制定对应的Theme(Theme是用来支持不同资源的Dark Mode切换的)。
1 | // Bundle加载 |
如果这个不满足,你也可以通过CALayer来做渲染,做更为细致的调节。并且VectorDrawable也提供了一些定制项(如设置tintColor)
优缺点
优点
- 能够和Android端复用,并且由于可由SVG生成,意味着Web端也可复用设计资源
- 性能良好,无论官方还是Example测试,除去CoreSVG外都是最快的渲染速度
缺点
- 目前iOS实现不支持动画(AnimatedVectorDrawable)
- 部分SVG实现VectorDrawable不支持,需要设计资源修改
- Uber内部开源,可能存在未来持续社区建设和维护成本,需要评估
SVG-Native
SVG-Native是由Adobe主导提出的一个W3C规范,目前处于Draft Stage,不过由于Apple,Google的赞同,大概率会在2020年内通过,并且正式规范定稿。
SVG-Native基于目前的SVG/1.1版本,是SVG/1.1的真子集(即一个SVG-Native图一定可以被浏览器正确渲染)。
注:曾经W3C有一个SVG Tiny的规范,但是它是针对移动浏览器场景的,和SVG-Native解决的问题是不一样的。
它针对移动平台,桌面平台等非浏览器场景做了针对性定制,废弃了一些Native端非常困难实现的功能,包括:
- scripting: 不依赖JavaScript环境
- animations: 不支持动画
- filters: 不支持滤镜,部分效果(如文字滤镜)依赖实现复杂
- masks: 不支持蒙层
- patterns: 不支持仿制图章,Color Pattern
- texts: 不内嵌文字,文字使用Path绘制
- events: 点击事件等,因为没有Script交互自然不需要
- CSS3:CSS3是一个完整布局系统,大量属性远远超过SVG的功能,如Flexbox,Media-Query,都是不必要的,只有基本的渲染属性
可以看出,这些剥离的功能都是和浏览器场景完全绑定的,不适用于通用的App内渲染矢量图的用途。SVG-Native更适合桌面/移动的App,渲染器实现也会精简很多,容易单元测试,并且可供操作系统内嵌集成。
使用
Adobe提供了一个目前Draft规范的渲染实现SVG Native Viewer,目前提供了多种渲染引擎的桥接,包括我们熟悉的CoreGraphics和Skia。
SVG-Native解码器,能够以标量图的方式,渲染SVG到一个指定大小的CGContext上,性能目前看足够快(和CoreSVG对比)。目前一般是通过重写drawRect来让View大小变化时进行重绘。
1 | - (void)drawRect:(NSRect)dirtyRect { |
优缺点
优点
- W3C规范,可以确保未来规范的准确性,并且操作系统提供商,如Apple更容易集成
- SVG-Native是SVG1.1的真子集,意味者可以复用到Web上
- SVG-Native会是未来的OpenType-SVG实现,意味着Adobe字体或者设计师群体更容易接受
缺点
- SVG-Native是SVG真子集,意味着目前的SVG设计资源,需要适配修改才可支持
- 截至目前,SVG-Native依然处于Draft阶段,稳定,推广普及需要较长时间
- SVG-Native目前只有Adobe的解析器实现,部分特性在CoreGraphics上工作并不良好
- 目前没有看到动画的支持
总结
总结一下关于SVG的相关解决方案,可以看出,没有一种Case能够涵盖所有场景,当然,这和Apple本身对矢量图支持的建设有一定关系,大部分建设依赖于开源社区。因此,通常情况下需要根据自己具体的实际需要来选择,比如:
- 只考虑Path,Circle等矢量路径:使用SwiftSVG、Macaw即可
- 考虑和Android复用:使用VectorDrawable
- 不考虑iOS 13以下兼容:优先用Symbol Image和CoreSVG
- 考虑SVG动画:Macaw
- 面向未来:SVG-Native
参考资料
- 解读 WWDC19 - SF Symbols 内置图标库
- SF Symbols: The benefits and how to use them guide
- SVG: Scalable Vector Graphics
- SDWebImageSVGCoder
- Vector drawables overview
- Introducing Cyborg, an Open Source iOS Implementation of Android VectorDrawable
- OpenType-SVG color fonts
- SVG Native: Open Sourcing SVG Native Viewer