Why UIImage(systemName:) returns nil — and how to make it impossible
You wrote this, ran the app, and the button is empty:
let image = UIImage(systemName: "arrow.uturn.backward") button.setImage(image, for: .normal)
UIImage(systemName:) is a failable initializer — it returns UIImage?. When it hands back nil, the system image silently doesn’t render. There are only a few reasons it returns nil, and almost all of them trace back to the same root cause: the symbol name is a string, and strings are easy to get wrong.
The four reasons it’s nil
- Misspelled name.
"arrow.uturn.bacward","arrow.uturn-backward","arrowUturnBackward". All compile. All returnnil. - Symbol doesn’t exist at all. You invented a plausible-sounding name, or copied it from an answer that was wrong.
- Symbol exists but not on this OS version. Apple introduces symbols per release. A symbol added in iOS 17 is
nilon an iOS 16 device. Your deployment target lets the code build; the runtime returnsnil. - Variant typo. The base symbol exists but the variant suffix is wrong:
"trash.fill"vs"trash.full".
Three of those four are spelling problems the compiler never had a chance to catch, because the parameter is String.
Diagnosing it right now
Quick triage when you hit a nil:
let name = "arrow.uturn.backward" if UIImage(systemName: name) == nil { assertionFailure("SF Symbol '\(name)' did not resolve — check spelling and availability") }
That at least surfaces the failure in debug instead of shipping a blank button. But it’s a band-aid: you’re validating a string at runtime that should have been validated at compile time. You still have to notice the assertion, for every symbol, forever.
Making the failure mode impossible
The durable fix is to stop passing raw strings. SFSymbolsKit exposes a typed property for every symbol Apple ships, plus a ready UIImage:
import SFSymbolsKit // Typed name — autocompleted, compile-checked let image = UIImage.SFSymbols.arrowUturnBackward button.setImage(image, for: .normal)
Reasons 1, 2 and 4 — the spelling failures — become impossible: a wrong identifier won’t compile. Reason 3 (OS availability) is still a real platform constraint, but now you’re reasoning about a known-correct symbol’s availability instead of also wondering whether you spelled it right.
The deeper point: UIImage(systemName:) returning an optional is Apple acknowledging at the type level that a string symbol name is untrustworthy input. The right response isn’t to handle the nil more gracefully — it’s to remove the untrusted string from your code entirely.
FAQ
Can SFSymbolsKit guarantee a symbol exists on my deployment target? It guarantees the name is real (it’s generated from Apple’s catalog). OS-version availability is still platform behavior — but you’ve eliminated the spelling class of nil entirely, which is the overwhelming majority of real-world cases.
Does the typed UIImage accessor still return an optional? It hands you a configured UIImage for the symbol without you threading systemName: strings through your code. You stop writing the failable string call by hand.
Is this just a constants file? It’s a generated one covering all ~7,000 symbols and kept in sync with Apple’s releases — so it doesn’t rot the way a hand-maintained file does.
Same root cause shows up everywhere SF Symbols touch a string API. The SwiftUI version of this problem is the silent blank Image; the tutorial walks through all three of Apple’s stringly-typed symbol APIs and the fix.