I’ve just released a new, small Swift library for loading and rendering a useful subset of SVG on macOS or iOS. I named it “Yass” - “yet another Swift SVG [library]”.
That “useful subset” consists of icons, drawn out of common shapes and paths, with simple colors; no scripting, gradients, text, animation, etc. See example icons below. I built it a few months ago, adding it to version 1.7 of my Mac app TimeStory, which has used it since, and I thought others might find it useful.
Yass is:
- Clean and “Swifty”: Yass mainly consists of a set of value types modeling SVG shapes, paths, and attributes, along with code to parse them from SVG and Core Graphics extensions to render them. See below for details.
- Not based on the SVG DOM: this is required for scripting or interop with other Web technologies, but I just didn’t really care
- Tiny: just a few files
- Focused: implements just enough of an SVG subset to render a set of simple icons, created in and exported by Sketch. (More complex drawings, or other SVG generators, may use features not included here). See below for examples.
- Able to accept colors and sizes when rendering, to allow stamping a common SVG asset in different places
- Standalone and platform-independent: depending only on Foundation (for XML parsing) for loading and manipulating shape data, and Core Graphics for rendering (I currently build it into the main AppKit-based TimeStory app for Mac, an incomplete UIKit-based TimeStory app for iOS, and even an internal command-line tool)
Yass’s design
As mentioned above, Yass uses Swift value types to model SVG data; its model
is built around a few enum
s defining the available elements, shapes, and
path instructions, and a few struct
s packaging up bundles of attributes.
Here are a couple of examples:
public enum SVGElement {
indirect case svg(String?, SVGFragmentAttributes, SVGPresentationAttributes, [SVGElement])
indirect case group(String?, SVGPresentationAttributes, [SVGElement])
case graphic(String?, SVGPresentationAttributes, SVGGraphic)
}
public enum SVGGraphic {
case path([SVGPathInstruction])
case rect(CGRect)
case circle(c: CGPoint, r: CGFloat)
case ellipse(c: CGPoint, rx: CGFloat, ry: CGFloat)
case line(p1: CGPoint, p2: CGPoint)
case polyline([CGPoint])
case polygon([CGPoint])
}
This means that the drawing and path-construction code can use switch
statements to cover all bases. For example, this is from my CGContext
extension:
public func svg_buildPath(_ graphic: SVGGraphic) {
beginPath()
switch graphic {
case .path(let instrs):
svg_buildPath(instrs)
case .rect(let rect):
addRect(rect)
case .circle(c: let c, r: let r):
addEllipse(in: CGRect(x: c.x - r, y: c.y - r,
width: r * 2, height: r * 2))
case .ellipse(c: let c, rx: let rx, ry: let ry):
addEllipse(in: CGRect(x: c.x - rx, y: c.y - ry,
width: rx * 2, height: ry * 2))
case .line(p1: let p1, p2: let p2):
move(to: p1)
addLine(to: p2)
case .polyline(let points):
addLines(between: points)
case .polygon(let points):
addLines(between: points)
closePath()
}
}
Contrast this approach with an object-oriented or protocol-oriented approach, more common in other SVG libraries that I looked at, where each SVG element or shape exposes a “draw” or “addToPath” method and encapsulates its implementation.
Here, drawings are described by data structures which are independent of
destination, and their types have no knowledge of Core Graphics (other than
the use of CGRect
, CGPoint
, and CGFloat
). The actual path-drawing code
lives in one file, which uses Swift switch
statements to pattern-match,
destructure, and recurse. Now imagine extending this library to support
SwiftUI by creating Path
objects, or even Cairo for non-Apple
platforms. Each of those would again be a simple, decoupled set of
pattern-matching code.
Example shapes
Here is a screenshot of TimeStory’s shape picker. The shapes following the octagon are all rendered by Yass from embedded SVG assets. (The first few are older shapes, which I implemented with direct Core Graphics path-drawing code; I didn’t feel the need to delete that code.)
When you add one of these to your document, it is given a size and color based on other properties.
The code
I use GitLab for all my development, private and public. Find Yass here. (I will probably set up a GitHub mirror at some point.) I have released it under the MIT license, so you can use this in your nonfree, closed-source apps, if you find it useful.
I’ve recently started using the Swift Package Manager to organize my own internal libraries. It’s quite nice, and Xcode’s integration with SPM works reasonably well. So Yass is packaged for SPM, with two targets, a static library and a dynamic framework; I use them both (the static library for my TimeStory CLI target, and the framework for my Mac and iOS targets).
(I don’t, at present, use CocoaPods or Carthage for anything, so no support for them out of the box.)
I’m happy to hear any input on this library, or just a quick note if you’re using it, but feel free to take it, modify it, and use it as you wish.