Crash Proof Your SF Symbols Code

Use these tweaks for crash proofing SF Symbols in your code.

Crash Proof Your SF Symbols Code

One moment, I was previewing my app in Xcode. Then the canvas crashed. The simulator crashed on launch. Before I knew it, 20 minutes were gone. All I had to show for it was debug logs and useless code changes I had to revert. The "bug" amounted to Image(systemName: "typoooo"). A typo in the string name of an SF Symbol in my tab bar. The bug itself wasn't painful to fix, but the time spent debugging it stung.

Avoid my pain. Use these tweaks for crash proofing SF Symbols in your code.

Define your symbols:

We need a source of truth for the SF symbols we plan to use. Make a new Swift file, then create a string backed enum. I called mine SFSymbols.

enum SFSymbols: String {
    case settings               = "gear" // Semantic alias
    case lSquareFill            = "l.square.fill"
    case questionmarkSquareFill = "questionmark.square.fill"
    case square
    case squareFill             = "square.fill"
    case _01SquareFill          = "01.square.file"
}
  • Backing the enum with a string is important, we will use the rawValue attribute of the enum cases in a moment.
  • You can create alias for a semantic term, such as changing "gear" to "settings".
  • The raw string value must exactly match what is provided by the SF Symbols App. Names like "gear" and "square" will work with the compilers synthesized raw value. For complex cases like "questionmark.square.fill", we need to help it out.
  • Starting cases with a number is invalid. I prefix those with an underscore.

Tip: Sort the cases alphabetically to avoid technical debt as the list grows larger.


Using your symbols: Image extension

Write an extension on Image which adds an initializer to Image views. It will accept the SFSymbols enum we created above as its input.

extension Image {
    init(sfSymbol: SFSymbols) {
        self.init(systemName: sfSymbol.rawValue)
    }
}

To use it: Image(sfSymbol: .settings).


Using your symbols: Custom view

If you don't want to write an extension on Image, you can write your own view.

struct SFSymbol: View {
    let name: SFSymbols
    
    var body: some View {
        Image(systemName: name.rawValue)
    }
}

To use it: SFSymbol(name: .settings)


Addendum A: What are stringly-typed APIs?

Stringly-typed APIs are:

  • Code that takes input in the form of a string.
  • Code that requires a finite set of input values.
  • Code that can't handle invalid input.

The Swift code for SF Symbols is an example of a stringly-typed API. You instantiate them using Image(systemName:) with the name of a symbol from the SF Symbols App as a string.

Pro tip: If you installed SF Symbols shortly after it was announced in 2019, you may need to manually update. Newer versions have more categories of symbols, automatic updates.

Addendum B: Why are stringly-typed APIs problematic?

When code requires input from a finite set of values, strings are not the ideal input type. A string could be anything. Finite inputs and strings are mutually exclusive concepts. Using a stringly-typed API means we need input validation, error handling, and could crash.

Not all stringly typed APIs are "bad," but I consider them to be a code smell. Some APIs have no choice, like if they are handling user input... But there are often better data types, like enums, to use when you require input from a finite set of values.

Using the default way to instantiate an SF Symbol, Image(systemName:)... Try using "settings" and your app will crash at runtime. Also problematic: "Gear", "GEAR", "gearr", and "geer". You get the idea.

Addendum C: Why are type safe APIs better?

It's my goal to use a data type which will have the compiler catch input errors. I can fix such problems before end users see them. If the compiler knows all the possible inputs, the autocomplete tools in Xcode know them too. Lastly, it's a single source of truth in the event I want to change an icon used in various parts across my apps.


Addendum D: The source code for this post:

import Foundation
import SwiftUI

enum SFSymbols: String {
    case _01SquareFill          = "01.square.file"
    case lSquareFill            = "l.square.fill"
    case questionmarkSquareFill = "questionmark.square.fill"
    case settings               = "gear" // Semantic alias
    case square
    case squareFill             = "square.fill"
}

extension Image {
    init(sfSymbol: SFSymbols) {
        self.init(systemName: sfSymbol.rawValue)
    }
}

struct SFSymbol: View {
    let name: SFSymbols
    
    var body: some View {
        Image(systemName: name.rawValue)
    }
}