-
Notifications
You must be signed in to change notification settings - Fork 0
Icons
DFIcon is DesignFoundation's unified icon primitive. It wraps SF Symbols and custom images behind a single, consistent API — enforcing theme-aware sizing, automatic rendering modes, and zero-config accessibility defaults.
Related pages: Primitives · Theming · Buttons · Text-Fields-and-Inputs · Lists-Tables-and-Data
- Overview
- Initializer Reference
- DFIconSize Reference
- Style Variants
- Color Guide
- SF Symbol Weights and Rendering Modes
- Custom Image Icons
- Accessibility
- Complete Examples
- Combining DFIcon with Other Primitives
| Concern | Image(systemName:) |
DFIcon |
|---|---|---|
| Sizing | Manual .font(.system(size: 20)) per call-site |
Semantic size tokens (xs…xl) from the theme |
| Color | Manual .foregroundStyle(…)
|
Inherits environment or one explicit param |
| Rendering mode | Default symbol rendering |
.hierarchical on iOS 18+ / macOS 15+ automatically |
| Accessibility | Announced as "image" to VoiceOver | Decorative by default (hidden); opt-in label when semantic |
| Custom images |
Image("logo") — different API |
DFIcon(image: Image("logo")) — same API |
// Before
Image(systemName: "star.fill")
.foregroundStyle(theme.colors.primary)
.font(.system(size: 20))
.accessibilityHidden(true)
// After
DFIcon("star.fill", size: .md, color: theme.colors.primary)DFIcon("house.fill") // md size, inherits foreground
DFIcon("house.fill", size: .lg)
DFIcon("house.fill", size: .lg, color: theme.colors.primary)DFIcon(image: Image("AppLogo"), size: .xl)
DFIcon(image: Image("BrandGlyph"), size: .md, color: theme.colors.primary) // template modepublic enum DFIconSize: Sendable {
case xs // 12 pt
case sm // 16 pt
case md // 20 pt ← default
case lg // 28 pt
case xl // 36 pt
}| Case | Point size | Recommended contexts |
|---|---|---|
.xs |
12 pt | Inline with .caption text, tight table cells |
.sm |
16 pt | Leading icons in DFTextField, DFListRow icons |
.md |
20 pt | Default — toolbar buttons, row actions |
.lg |
28 pt | Section headers, card header icons |
.xl |
36 pt | Empty state heroes, feature highlights |
HStack(spacing: theme.spacing.md) {
DFIcon("star.fill", size: .xs)
DFIcon("star.fill", size: .sm)
DFIcon("star.fill", size: .md)
DFIcon("star.fill", size: .lg)
DFIcon("star.fill", size: .xl)
}Renders the symbol at its natural weight. Use in nav bars, list rows, alongside text.
DFIcon("bell") // standard, implicitIcon centered inside a filled circle container. Best for empty states and feature highlights.
DFIcon("star.fill", size: .xl).dfIconStyle(.circle)
DFIcon("checkmark", size: .md, color: theme.colors.success).dfIconStyle(.circle)
// Empty state pattern
VStack(spacing: theme.spacing.lg) {
DFIcon("tray", size: .xl, color: theme.colors.textSecondary)
.dfIconStyle(.circle)
DFText("No messages", style: .headline)
}Automatically appends .fill to the symbol name if not already present.
DFIcon("heart").dfIconStyle(.filled) // renders heart.fill
DFIcon("star").dfIconStyle(.filled) // renders star.fill
// Toggle filled/unfilled based on state
DFIcon("heart", size: .md, color: isLiked ? theme.colors.error : theme.colors.textSecondary)
.dfIconStyle(isLiked ? .filled : .standard)// Default: inherits foreground
DFIcon("house.fill")
// Brand accent
DFIcon("sparkles", size: .md, color: theme.colors.primary)
// Muted / decorative
DFIcon("magnifyingglass", size: .sm, color: theme.colors.textSecondary)
// Semantic colors — prefer these over raw Color for status
DFIcon("checkmark.circle.fill", size: .sm, color: theme.colors.success)
DFIcon("exclamationmark.triangle.fill", size: .sm, color: theme.colors.warning)
DFIcon("xmark.circle.fill", size: .sm, color: theme.colors.error)Decision guide:
- Status (success / warning / error)? →
theme.colors.success / .warning / .error - Primary action or active selection? →
theme.colors.primary - Decorative or supplementary? →
theme.colors.textSecondary - Match surrounding text? → omit color, inherit foreground
DFIcon uses .hierarchical rendering mode on iOS 18+ / macOS 15+ automatically. Override when needed:
// Monochrome (override hierarchical)
DFIcon("cloud.sun.rain.fill", size: .lg, color: theme.colors.primary)
.symbolRenderingMode(.monochrome)
// Palette — two explicit colors
DFIcon("person.badge.plus", size: .lg)
.symbolRenderingMode(.palette)
.foregroundStyle(theme.colors.primary, theme.colors.textSecondary)
// Bold weight for emphasis
DFIcon("bolt.fill", size: .md, color: theme.colors.warning)
.fontWeight(.bold)// Asset catalog image
DFIcon(image: Image("BrandMark"), size: .lg)
// From UIImage
DFIcon(image: Image(uiImage: downloadedImage), size: .md)Mark the asset as Template Image in Xcode to allow tinting:
// Template image: color param takes effect
DFIcon(image: Image("CheckIcon"), size: .sm, color: theme.colors.success)DFIcon is hidden from VoiceOver by default — correct for decorative icons adjacent to text.
// Standalone semantic icon — needs a label
Button { deleteItem() } label: {
DFIcon("trash", size: .md, color: theme.colors.error)
.accessibilityLabel("Delete item")
.accessibilityHidden(false)
}
// Status icon with no adjacent text
DFIcon("checkmark.circle.fill", size: .sm, color: theme.colors.success)
.accessibilityLabel("Delivered")
.accessibilityHidden(false)Checklist:
- Icon alongside visible
DFText? → leave decorative (default) - Icon is the only content in a tappable element? →
.accessibilityLabel+.accessibilityHidden(false) - Status icon with no label in a table cell? →
.accessibilityLabel+.accessibilityHidden(false)
private let tabItems: [DFTabItem] = [
DFTabItem(id: "home", icon: "house.fill", label: "Home"),
DFTabItem(id: "explore", icon: "safari.fill", label: "Explore"),
DFTabItem(id: "messages", icon: "message.fill", label: "Messages"),
DFTabItem(id: "alerts", icon: "bell.fill", label: "Alerts"),
DFTabItem(id: "profile", icon: "person.fill", label: "Profile"),
]DFTextField("Email address", text: $email, leadingIcon: "envelope")
DFTextField("Search", text: $query, leadingIcon: "magnifyingglass")
DFSecureField("Password", text: $password) // lock icon built infunc statusIcon(_ status: OrderStatus) -> some View {
switch status {
case .delivered:
DFIcon("checkmark.circle.fill", size: .sm, color: theme.colors.success)
.accessibilityLabel("Delivered").accessibilityHidden(false)
case .failed:
DFIcon("xmark.circle.fill", size: .sm, color: theme.colors.error)
.accessibilityLabel("Failed").accessibilityHidden(false)
case .pending:
DFIcon("clock.fill", size: .sm, color: theme.colors.warning)
.accessibilityLabel("Pending").accessibilityHidden(false)
}
}VStack(spacing: theme.spacing.xl) {
DFIcon("tray", size: .xl, color: theme.colors.textSecondary)
.dfIconStyle(.circle)
VStack(spacing: theme.spacing.sm) {
DFText("Nothing here yet", style: .headline)
DFText("Messages you receive will appear here.", style: .caption)
.multilineTextAlignment(.center)
}
DFButton("Compose", icon: "square.and.pencil") { onCompose() }
}
.padding(theme.spacing.xxl)
.frame(maxWidth: 320)struct SectionHeader: View {
let systemName: String
let title: String
@Environment(\.dfTheme) private var theme
var body: some View {
HStack(spacing: theme.spacing.sm) {
DFIcon(systemName, size: .lg, color: theme.colors.primary)
DFText(title, style: .headline)
Spacer()
}
.padding(.bottom, theme.spacing.xs)
}
}struct FeatureRow: View {
let feature: String
let hasBasic: Bool
let hasPro: Bool
@Environment(\.dfTheme) private var theme
var body: some View {
HStack {
DFText(feature, style: .body)
Spacer()
tierIcon(included: hasBasic).frame(width: 60)
tierIcon(included: hasPro).frame(width: 60)
}
.padding(.vertical, theme.spacing.xs)
}
@ViewBuilder
private func tierIcon(included: Bool) -> some View {
Group {
if included {
DFIcon("checkmark.circle.fill", size: .sm, color: theme.colors.success)
} else {
DFIcon("minus.circle", size: .sm, color: theme.colors.border)
}
}
.frame(maxWidth: .infinity, alignment: .center)
}
}struct AppHeader: View {
@Environment(\.dfTheme) private var theme
var body: some View {
HStack(spacing: theme.spacing.sm) {
DFIcon(image: Image("AppWordmark"), size: .lg) // original rendering
DFDivider().frame(height: 24)
DFText("Dashboard", style: .headline)
Spacer()
DFIcon(image: Image("BrandGlyph"), size: .md, color: theme.colors.primary) // template
}
.padding(.horizontal, theme.spacing.lg)
.background(theme.colors.surface)
}
}// Bounce on tap (iOS 17+ / macOS 14+)
Button { isFavorited.toggle() } label: {
DFIcon(
isFavorited ? "heart.fill" : "heart",
size: .lg,
color: isFavorited ? theme.colors.error : theme.colors.textSecondary
)
.symbolEffect(.bounce, value: isFavorited)
.accessibilityLabel(isFavorited ? "Remove from favorites" : "Add to favorites")
.accessibilityHidden(false)
}
.buttonStyle(.plain)
// Pulse for live state
DFIcon("antenna.radiowaves.left.and.right", size: .md, color: theme.colors.success)
.symbolEffect(.pulse)
// Replace animation on symbol name change
DFIcon(isFavorited ? "star.fill" : "star", size: .md, color: theme.colors.primary)
.contentTransition(.symbolEffect(.replace))struct LabeledIconRow: View {
let systemName: String
let label: String
let value: String
@Environment(\.dfTheme) private var theme
var body: some View {
HStack(spacing: theme.spacing.sm) {
DFIcon(systemName, size: .sm, color: theme.colors.textSecondary)
DFText(label, style: .body)
Spacer()
DFText(value, style: .body).foregroundStyle(theme.colors.textSecondary)
}
}
}struct NotificationIcon: View {
let count: Int
@Environment(\.dfTheme) private var theme
var body: some View {
ZStack(alignment: .topTrailing) {
DFIcon("bell.fill", size: .md, color: theme.colors.textPrimary)
if count > 0 {
DFBadge(text: count > 99 ? "99+" : "\(count)", color: .red)
.offset(x: 8, y: -6)
}
}
}
}HStack(spacing: theme.spacing.md) {
DFIcon("sparkles", size: .lg, color: theme.colors.primary)
.dfIconStyle(.circle)
VStack(alignment: .leading, spacing: theme.spacing.xs) {
HStack(spacing: theme.spacing.sm) {
DFText("AI Writing Assistant", style: .headline)
DFBadge(text: "New", color: .purple)
}
DFText("Draft emails and messages in seconds.", style: .caption)
}
Spacer()
DFIcon("chevron.right", size: .sm, color: theme.colors.textSecondary)
}
.padding(theme.spacing.md)DFIcon("star.fill", size: .xs) // 12 pt
DFIcon("star.fill", size: .sm) // 16 pt
DFIcon("star.fill", size: .md) // 20 pt (default)
DFIcon("star.fill", size: .lg) // 28 pt
DFIcon("star.fill", size: .xl) // 36 pt
DFIcon("star.fill").dfIconStyle(.standard) // default
DFIcon("star.fill").dfIconStyle(.circle) // circular container
DFIcon("star").dfIconStyle(.filled) // auto → star.fill
DFIcon("star.fill") // inherits foreground
DFIcon("star.fill", color: theme.colors.primary) // brand accent
DFIcon("star.fill", color: theme.colors.textSecondary) // muted
DFIcon("star.fill", color: theme.colors.success) // semantic green
// Accessibility
DFIcon("trash") // decorative (default)
DFIcon("trash")
.accessibilityLabel("Delete")
.accessibilityHidden(false) // semanticSee also: Primitives · Buttons · Text-Fields-and-Inputs · Lists-Tables-and-Data · Navigation · Theming