Tutorial · 8 min read

SF Symbols in Swift: A Type-Safe Guide

Apple ships SF Symbols as stringly-typed APIs across SwiftUI, UIKit, and AppKit. This guide walks through the four ways developers handle them today, why each one falls short, and how to make the whole problem go away with SFSymbolsKit.

What are SF Symbols?

SF Symbols is Apple's iconography system — a curated catalog of over 7,000 icons designed to integrate with the system font (SF Pro). Apple ships them with every operating system from iOS 13 onwards, and they're free to use in any app that ships on Apple platforms. Each symbol has a unique string name like plus.app, xmark.circle.fill, or arrow.up.right. You reference them through systemName: initializers on Image, UIImage, and NSImage.

The SF Symbols app (free on the Mac App Store) is the canonical browser. You search for the symbol you want, copy its name, and paste it into your code. That paste step is where the problems start.

The problem with Apple's API

Apple's symbol APIs are all stringly-typed:

// SwiftUI
Image(systemName: "plus.app")

// UIKit
let icon = UIImage(systemName: "plus.app")

// AppKit
let icon = NSImage(systemSymbolName: "plus.app", accessibilityDescription: nil)

Three things stand out:

  1. No autocomplete. Xcode can't suggest "plus.app" for you. You either remember it, copy it from the SF Symbols app, or hope you typed it right.
  2. No compile-time check. If you typo the name — "pls.app", "plus_app", "plusapp" — the compiler accepts it. It's a String. Strings are valid.
  3. No runtime error. UIImage(systemName:) returns UIImage? when the symbol doesn't exist. Image(systemName:) just renders blank. Neither tells you anything went wrong. Your icon is missing in production and the only signal is "hey, the button looks weird".

Compound this across hundreds of icons in a real app, and "did I type that right" becomes a non-trivial source of bugs that slip past code review because they look correct in the diff.

The four approaches developers take

Approach 1: Just type the string each time

struct TabBar: View {
    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Image(systemName: "house")
                    Text("Home")
                }
            SettingsView()
                .tabItem {
                    Image(systemName: "gera")  // 👈 typo. Compiler doesn't care.
                    Text("Settings")
                }
        }
    }
}

Zero dependencies, zero setup. Also zero protection. Most apps start here and pay for it later when a designer notices a missing icon two weeks before shipping.

Approach 2: A hand-rolled constants enum

enum AppSymbols {
    static let house = "house"
    static let gear = "gear"
    static let plusApp = "plus.app"
    // ... and 47 more
}

Image(systemName: AppSymbols.gear)  // ✓ typed

This is the standard senior-engineer move. It fixes the typo problem inside your codebase, but it introduces three new ones:

Approach 3: Copy names from the SF Symbols app

The "visual lookup" approach. You open the SF Symbols app, search "settings", click the gear, hit ⌘C to copy the name, paste into your code. This solves the discoverability half of the problem but still produces stringly-typed code downstream. The typo bug doesn't disappear; it just moves from "you remembered wrong" to "you pasted from the wrong field". Still no compile-time check.

Approach 4: Use SFSymbolsKit

import SFSymbolsKit

Image(systemName: String.SFSymbols.house)
Image(systemName: String.SFSymbols.gear)
Image(systemName: String.SFSymbols.plusApp)

SFSymbolsKit ships every SF Symbol Apple has ever shipped as a typed Swift property. Xcode autocompletes. The compiler catches typos. When Apple ships a new release with new symbols, regenerating the package picks them all up at once. You get all the benefits of Approach 2 with none of the maintenance burden.

SFSymbolsKit by example

Adding SFSymbolsKit to your project

SFSymbolsKit is a Swift Package. Add it to your Package.swift:

dependencies: [
    .package(url: "https://github.com/WikipediaBrown/SFSymbolsKit.git",
             from: "0.1.26")
]

Or in Xcode: File → Add Package Dependencies… and paste https://github.com/WikipediaBrown/SFSymbolsKit.git.

Example 1: A SwiftUI tab bar that can't have icon typos

import SwiftUI
import SFSymbolsKit

struct RootView: View {
    var body: some View {
        TabView {
            HomeView()
                .tabItem {
                    Label("Home", systemImage: String.SFSymbols.house)
                }
            SearchView()
                .tabItem {
                    Label("Search", systemImage: String.SFSymbols.magnifyingglass)
                }
            SettingsView()
                .tabItem {
                    Label("Settings", systemImage: String.SFSymbols.gear)
                }
        }
    }
}

Every systemImage here is a typed Swift property. There is no string version of the name floating around in your codebase. If you rename one of these in the future, the compiler tells you what else broke.

Example 2: A UIKit button icon that fails at compile time, not runtime

import UIKit
import SFSymbolsKit

final class ComposeButton: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        // Get a UIImage directly — no UIImage(systemName:) wrapping needed.
        setImage(UIImage.SFSymbols.squareAndPencil, for: .normal)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

The UIImage.SFSymbols.* extension returns a configured UIImage. No optional unwrapping, no dance through systemName:. It works on iOS, iPadOS, tvOS, watchOS, and visionOS.

Example 3: An AppKit menu item with accessibility wired up automatically

import AppKit
import SFSymbolsKit

func buildPreferencesItem() -> NSMenuItem {
    let item = NSMenuItem(title: "Preferences…",
                          action: #selector(AppDelegate.openPreferences),
                          keyEquivalent: ",")
    item.image = NSImage.SFSymbols.gear
    return item
}

On macOS, the NSImage extension synthesises the accessibility description from the symbol's name. VoiceOver readers get a sensible label without you remembering to set one.

Example 4: A symbol picker over all 7,000+ icons

import SwiftUI
import SFSymbolsKit

struct SymbolPicker: View {
    @Binding var selection: SFSymbol

    var body: some View {
        List(SFSymbol.allCases, id: \.self, selection: $selection) { symbol in
            Label(symbol.rawValue, systemImage: symbol.rawValue)
        }
    }
}

The SFSymbol enum is CaseIterable, so you can present every symbol in a list, a grid, or a picker without maintaining your own catalog. The same enum also exposes a .image property that returns the appropriate UIImage or NSImage for the current platform.

Why SFSymbolsKit beats the alternatives

Concern Apple's API (strings) Hand-rolled enum SFSymbolsKit
Autocomplete ✓ (for symbols you added) ✓ (all 7,000+)
Catches typos at compile time
Every Apple-shipped symbol included Depends on your patience
Auto-updates when Apple ships new symbols Implicitly (find out in QA) ❌ — manual ✓ — regenerate
UIImage / NSImage directly
CaseIterable for pickers Partial
Maintenance burden Low High Low
Runtime cost Zero Zero (string literals)
Transitive dependencies 0 0 0

The trade-off SFSymbolsKit makes is straightforward: it adds one Swift Package dependency to your project. In exchange, you stop hand-maintaining a constants file and stop shipping silent icon bugs. For most teams, that's a one-time five-minute setup that pays back the first time someone would have typed "pls.app".

Frequently asked questions

Is SFSymbolsKit production-ready?

Yes. It's MIT-licensed, has no transitive dependencies, and the Swift sources are generated deterministically from a single text file — so there's no risk of hand-typed regressions. The latest version is v0.1.26.

Which Apple platforms does SFSymbolsKit support?

iOS 14+, iPadOS 14+, macOS 10.13+, tvOS, watchOS, and visionOS. The String extension is available everywhere; the UIImage extension activates wherever UIKit is present, and the NSImage extension wherever AppKit is.

Does adding 7,000+ typed properties slow down compile times?

Not measurably. The String properties are static let bindings to literal strings — the Swift compiler treats those almost identically to inline strings at the call site. The UIImage/NSImage extensions wrap the system initializers; they're cheap.

How are new SF Symbols added to SFSymbolsKit?

The repository contains SFSymbols.txt — the canonical list of symbol names — and a set of Python scripts that generate the Swift source from it. When Apple ships new icons, update SFSymbols.txt and rerun the generator. Pull requests are welcome at the GitHub repo.

Can I mix SFSymbolsKit with custom symbols I designed myself?

Yes. SFSymbolsKit only covers Apple's catalog. For custom symbols you've designed and added to your asset catalog, keep using UIImage(named:) or Image(_:). SFSymbolsKit doesn't replace those APIs; it replaces the stringly-typed system-symbol ones.

Why not just publish a Constants.swift file in our own app?

That's effectively what SFSymbolsKit is — except it's already written, kept in sync with Apple's catalog through regeneration, comes with UIImage/NSImage helpers and a CaseIterable enum, and is one .package(url:) line away. If your team enjoys maintaining the constants file by hand, that's a valid choice. SFSymbolsKit just removes the chore.

How does SFSymbolsKit compare to other Swift packages that wrap SF Symbols?

Most alternatives are either incomplete (covering a subset of symbols), hand-maintained (so they drift behind Apple's releases), or take a manual approach to UIImage/NSImage. SFSymbolsKit is generated from the full SFSymbols.txt list and ships String, UIImage, and NSImage APIs as a single coherent set.

Does using SFSymbolsKit affect my app's SF Symbols license obligations?

No. SFSymbolsKit only wraps the names; it doesn't redistribute the symbol artwork itself. Apple's SF Symbols license terms apply to your app exactly as they would if you were using Image(systemName:) directly.

Get started

Three steps to type-safe SF Symbols across your entire Apple-platform codebase:

  1. Add the Swift Package dependency to your project (snippet below).
  2. Add import SFSymbolsKit at the top of any file that uses SF Symbols.
  3. Replace Image(systemName: "name") with Image(systemName: String.SFSymbols.name). Or, for UIKit, replace UIImage(systemName: "name") with UIImage.SFSymbols.name. Same shape for AppKit.
dependencies: [
    .package(url: "https://github.com/WikipediaBrown/SFSymbolsKit.git",
             from: "0.1.26")
]

For the complete API surface, see the SFSymbolsKit reference documentation. The source — and the Python generator — is on GitHub.