Overview:

In this new series on Swift code topics, the steps to create a custom symbol using an exported SF Symbol as a starting template are described. Sample SwiftUI code is included for displaying and dynamically modifying the size, weight and color of standard SF Symbols and compatible custom symbols.

The concept, app and Xcode 11 support for SF Symbols were introduced at WWDC 2019 along with a very large set of changes and updates to nearly everything else in the Xcode development world. WWDC 2019 was amazing in every aspect from a developer’s perspective and Apple’s “blown brain” logo was completely appropriate.

SF Symbols:

SF Symbols is a set of over 1500 vector-based symbol images (icons and glyphs) that can be used as icons during app development in Xcode 11. Each symbol image is defined by three scales (small, medium, large) and nine weights (Ultralight, Light, Regular, Medium, Semibold, Bold, Heavy, Black) that correspond to available scales and weights for the San Francisco system fonts. As such, SwiftUI code for SF Symbols can be integrated with surrounding text to keep size, weight, alignment and color consistent.

The SF Symbol App:

Using Apple’s SF Symbols App for Mac OS, it is easy to browse through the 1500+ SF Symbols and identify a symbol name to use in Xcode. The following SwiftUI code shows an example of including a SF Symbol (e.g., calendar) in a SwiftUI view within Xcode:

Image(systemName: "calendar")
    .imageScale(.medium)
    .font(Font.title.weight(.regular))
    .foregroundColor(.yellow)

In this example code, SwiftUI modifiers are used to select the scale, weight, and color for the symbol.

Creating a custom symbol:

Even with 1500+ symbols, being able to design custom symbols is still needed. Several steps are necessary to create a custom symbol that is consistent with the system provided SF Symbols with respect to scales, weights, and alignment.

Any symbol in the SF Symbols app can be exported as an SVG file. If the exported SVG file is then imported into a vector graphics app, such as Affinity Designer in my case, then all 27 scale and weight combinations for the symbol (e.g. calendar) can be seen in the screenshot from Affinity Designer below:

To highlight the steps in creating a custom symbol, a calendar SF Symbol will be exported and used as a template to create a calculator symbol in Affinity Designer (ref: What’s in your toolbox?) Here are the steps:

Use the SF Symbols App to select a symbol to export.

After launching the SF Symbols app, I found the calendar symbol and selected it. The export command is located under the File menu (File -> Export Symbol) or via shortcut key (CMD-E) and results in an exported SVG file (ref: an XML-based vector image format file for 2-dimensional graphics) file.

Learn the SVG format for a Symbol file

Since SVG is an XML-based file, it can be opened in a text editor to inspect its contents and formats. For the custom calculator symbol to be compatible for importing into Xcode’s asset catalog, its SVG file format must follow a set of custom symbol guidelines established by Apple. Each SVG file is divided into three main grouped layers: Symbols, Guides, and Notes. This top-level file format for the calendar.svg file is shown below (detailed sublayers have been omitted for readability):

<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 123-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
       "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="3300" height="2200">
 <!--glyph: "uni100249.medium", point size: 100.000000, font version: "Version 15.0d7e11", template writer version: "5"-->
<g id="Notes"> ... </g>
<g id="Guides"> ... </g>
<g id="Symbols"> ... </g>
</svg>

Expanding into the Symbols group, all 27 scale/weight variants of the calendar symbol are listed (detailed sublayers have been omitted for readability). Note that each variant is bracketed in a group.

 <g id="Symbols">
  <g id="Black-L" transform="matrix(1 0 0 1 2849.17 1556)"> ... </g>
  <g id="Heavy-L" transform="matrix(1 0 0 1 2553.8 1556)"> ... </g>
  <g id="Bold-L" transform="matrix(1 0 0 1 2258.56 1556)"> ... </g>
  <g id="Semibold-L" transform="matrix(1 0 0 1 1962.87 1556)"> ... </g>
  <g id="Medium-L" transform="matrix(1 0 0 1 1666.94 1556)"> ... </g>
  <g id="Regular-L" transform="matrix(1 0 0 1 1371.23 1556)"> ... </g>
  <g id="Light-L" transform="matrix(1 0 0 1 1074.89 1556)"> ... </g>
  <g id="Thin-L" transform="matrix(1 0 0 1 778.639 1556)"> ... </g>
  <g id="Ultralight-L" transform="matrix(1 0 0 1 482.148 1556)"> ... </g>
  <g id="Black-M" transform="matrix(1 0 0 1 2866.53 1126)"> ... </g>
  <g id="Heavy-M" transform="matrix(1 0 0 1 2571.19 1126)"> ... </g>
  <g id="Bold-M" transform="matrix(1 0 0 1 2275.99 1126)"> ... </g>
  <g id="Semibold-M" transform="matrix(1 0 0 1 1980.33 1126)"> ... </g>
  <g id="Medium-M" transform="matrix(1 0 0 1 1684.4 1126)"> ... </g>
  <g id="Regular-M" transform="matrix(1 0 0 1 1388.74 1126)"> ... </g>
  <g id="Light-M" transform="matrix(1 0 0 1 1092.32 1126)"> ... </g>
  <g id="Thin-M" transform="matrix(1 0 0 1 796.046 1126)"> ... </g>
  <g id="Ultralight-M" transform="matrix(1 0 0 1 499.53 1126)"> ... </g>
  <g id="Black-S" transform="matrix(1 0 0 1 2881.11 696)"> ... </g>
  <g id="Heavy-S" transform="matrix(1 0 0 1 2585.39 696)"> ... </g>
  <g id="Bold-S" transform="matrix(1 0 0 1 2289.81 696)"> ... </g>
  <g id="Semibold-S" transform="matrix(1 0 0 1 1993.88 696)"> ... </g>
  <g id="Medium-S" transform="matrix(1 0 0 1 1697.75 696)"> ... </g>
  <g id="Regular-S" transform="matrix(1 0 0 1 1401.8 696)"> ... </g>
  <g id="Light-S" transform="matrix(1 0 0 1 1105.36 696)"> ... </g>
  <g id="Thin-S" transform="matrix(1 0 0 1 809.01 696)"> ... </g>
  <g id="Ultralight-S" transform="matrix(1 0 0 1 512.494 696)"> ... </g>

  

Continuing to drill down, the definition of the Regular-M variant of the calendar symbol is shown.

<g id="Regular-M" transform="matrix(1 0 0 1 1388.74 1126)">
   <path d="M 27.7832 9.76562 L 94.4824 9.76562 C 104.688 9.76562 109.766 4.6875 109.766 -5.37109 L 109.766 -64.9902
        C 109.766 -75.0488 104.688 -80.127 94.4824 -80.127 L 27.7832 -80.127 C 17.5781 -80.127 12.4512 -75.0977 12.4512 -64.9902
        L 12.4512 -5.37109 C 12.4512 4.73633 17.5781 9.76562 27.7832 9.76562 Z M 27.0508 1.9043 C 22.7051 1.9043 20.3125 -0.390625 20.3125 -4.93164
            
         ...
            
        C 72.3633 -8.30078 72.9492 -8.83789 72.9492 -10.5469 L 72.9492 -13.4277 C 72.9492 -15.1367 72.3633 -15.625 70.6543 -15.625
        L 67.8223 -15.625 C 66.0645 -15.625 65.5273 -15.1367 65.5273 -13.4277 L 65.5273 -10.5469 C 65.5273 -8.83789 66.0645 -8.30078 67.8223 -8.30078 Z"/>
</g>

Import Symbol file into Affinity Designer and Modify

Here are the steps to modify the symbol variants in Affinity Designer and maintain compliance to the Xcode file format:

  • Zoom into one of the variants (e.g., Regular-M)
  • Use two-finger click to display the context menu. Select Geometry -> Separate Curves. This will create a list of curves for each object (in this specific case with the calendar symbol, 12 objects consisting of a outer rounded rectangle, inner rounded rectangle (i.e., monthly cutout) and 10 individual rounded rectangles (i.e., days).
  • If these objects are recombined using Geometry -> Combine Curves, the exported SVG file from Affinity Designer will not be Xcode compliant. As shown in the format below, the Regular-M variant of the image is missing the group wrapper.
<path id="Regular-M" d="M1416.52,1135.77L1483.22,1135.77C1493.43,1135.77 1498.51,1130.69 1498.51,1120.63L1498.51,1061.01
    C1498.51,1050.95 1493.43,1045.87 1483.22,1045.87L1416.52,1045.87C1406.32,1045.87 1401.19,1050.9 1401.19,1061.01L1401.19,1120.63
    C1401.19,1130.74 1406.32,1135.77 1416.52,1135.77ZM1415.79,1127.9C1411.44,1127.9 1409.05,1125.61 1409.05,1121.07L1409.05,1075.02

     ...

    C1461.1,1117.7 1461.69,1117.16 1461.69,1115.45L1461.69,1112.57C1461.69,1110.86 1461.1,1110.38 1459.39,1110.38L1456.56,1110.38
    C1454.81,1110.38 1454.27,1110.86 1454.27,1112.57L1454.27,1115.45C1454.27,1117.16 1454.81,1117.7 1456.56,1117.7Z"
    style="fill-rule:nonzero;"/>

  • If we also group the Regular-M variant of the image in Affinity Designer, then the exported SVG file from Affinity Designer is now Xcode compliant. As shown in the format below, the Regular-M variant of the image includes the group wrapper. Note that the transformation matrix is missing, but the relative offsets have been adjusted for all parameters in each path.
<g id="Regular-M">
    <path d="M1416.52,1135.77L1483.22,1135.77C1493.43,1135.77 1498.51,1130.69 1498.51,1120.63
        L1498.51,1061.01C1498.51,1050.95 1493.43,1045.87 1483.22,1045.87L1416.52,1045.87
        C1406.32,1045.87 1401.19,1050.9 1401.19,1061.01L1401.19,1120.63

         ...

        C1454.81,1110.38 1454.27,1110.86 1454.27,1112.57L1454.27,1115.45
        C1454.27,1117.16 1454.81,1117.7 1456.56,1117.7Z" style="fill-rule:nonzero;"/>
</g>

At this point, we have the recipe to modify the image variants using Affinity Designer (similar recipes exist for other vector-based graphics software) and create a new SVG symbol file. The following diagram shows the modifications made to the calendar Regular-M variant to create the calendar image. Now, we need to modify all the remaining 26 image variants. Although each of the weight variants for the symbol are individually designed, Apple notes that the scale variants can be computed as a scale factor: Small is 0.783 times Regular, Large is 1.29 times Regular.

Verify new symbol file imports into Xcode’s Asset Catalog

The final step is to drag the newly created SVG file into Xcode’s asset catalog. If it imports cleanly, all 27 image variants are visible in the asset catalog as shown below for the calculator symbol example.

SwiftUI code for standard SF Symbols and custom symbols:

The code for inserting a standard SF Symbol using SwiftUI and adding modifiers is clean and swifty. A little more code is needed to process a custom symbol similarly in SwiftUI. Both implementations are shown below:

import SwiftUI

struct ContentView: View {
    
    let calculatorSymbolConfig = UIImage.SymbolConfiguration(pointSize: 24.0, weight: .regular, scale: .medium)
    
    var body: some View {
        HStack(alignment: .top) {
            VStack {
                VStack {
                    Image(uiImage: UIImage(named: "calculator", in: nil, with: calculatorSymbolConfig)!.withRenderingMode(.alwaysTemplate))
                        .foregroundColor(.yellow)
                    Text("Hello World")
                        .font(Font.body.weight(.light))
                        .foregroundColor(Color(.label))
                }
                .padding()
                VStack {
                    Image(systemName: "calendar")
                        .imageScale(.medium)
                        .font(Font.title.weight(.regular))
                        .foregroundColor(.yellow)
                    Text("Hello everyone")
                        .font(Font.body.weight(.light))
                        .foregroundColor(Color(.label))
                }
                .padding()
            }
            .background(Color(UIColor.systemGray4))
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

A screenshot of this code showing both symbols (calendar and calculator) is shown below:

Summary:

With a little perseverance, the steps for creating a custom symbol are easily executed. The benefit is that the custom symbol supports all the dynamic typing (scale, weight, color) almost as simple as using standard SF Symbols.

Reference Material:

Creating Custom Symbol Images for your App by Apple.

Configuring and Displaying Symbol Images in your UI by Apple.

Human Interface Guidelines: SF Symbols by Apple.

SF Symbols: The benefits and how to use them guide written by Antoine van der Lee.

SF Symbols written by Ivy Knight.

WWDC 2019 Session 206: Introducing SF Symbols presented by Paolo Mazzetti and Tom Adriaenssen.