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:
- 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. - 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. - No runtime error.
UIImage(systemName:)returnsUIImage?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:
- Maintenance burden. Every time a designer wants a new icon, someone hand-types another constant. Multiply by every new feature.
- Drift. When Apple ships new symbols in iOS 27, your
AppSymbolsfile doesn't know about them until you remember to update. - Incomplete. Your constants file covers the symbols you've thought to add. The 6,900 symbols you haven't are still stringly-typed when you eventually need them.
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:
- Add the Swift Package dependency to your project (snippet below).
- Add
import SFSymbolsKitat the top of any file that uses SF Symbols. - Replace
Image(systemName: "name")withImage(systemName: String.SFSymbols.name). Or, for UIKit, replaceUIImage(systemName: "name")withUIImage.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.