Files
Matthias a60a76b797 Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
2026-04-19 21:11:32 +02:00

594 lines
22 KiB
Markdown

---
name: axiom-sf-symbols
description: Use when implementing SF Symbols rendering modes, symbol effects, animations, custom symbols, or troubleshooting symbol appearance - covers the full symbol effects system from iOS 17 through SF Symbols 7 Draw animations in iOS 26
license: MIT
compatibility: iOS 17+, iOS 18+ (Wiggle/Rotate/Breathe), iOS 26+ (Draw animations)
metadata:
version: "1.0.0"
---
# SF Symbols — Effects, Rendering, and Custom Symbols
## When to Use This Skill
Use when:
- Choosing between rendering modes (Monochrome, Hierarchical, Palette, Multicolor)
- Implementing symbol effects or animations (Bounce, Pulse, Scale, Wiggle, Rotate, Breathe, Draw)
- Working with SF Symbols 7 Draw On/Off animations
- Creating custom symbols in the SF Symbols app
- Troubleshooting symbol colors, effects not playing, or weight mismatches
- Deciding which effect matches a specific UX purpose
- Handling accessibility with symbol animations (Reduce Motion)
#### Related Skills
- Use `axiom-sf-symbols-ref` for complete API reference with all modifiers, UIKit equivalents, and platform availability matrix
- Use `axiom-swiftui-animation-ref` for general SwiftUI animation (not symbol-specific)
- Use `axiom-hig-ref` for broader icon design guidelines
## Example Prompts
#### 1. "My SF Symbol shows as a single flat color but I want it to have depth with multiple shades. How do I fix this?"
> The skill covers rendering mode selection — Hierarchical for depth from a single color, Palette for explicit per-layer colors
#### 2. "I want my download button to animate when tapped, then show a spinning indicator while downloading, and animate to a checkmark when done."
> The skill covers effect selection: Bounce for tap feedback, Breathe/Pulse for in-progress, Replace with content transition for completion
#### 3. "I'm trying to use the new Draw animations from SF Symbols 7 but the effect isn't playing."
> The skill covers Draw On/Off implementation, playback modes, iOS 26 requirements, and common troubleshooting
#### 4. "How do I create a custom symbol that supports all rendering modes and the new Draw animation?"
> The skill covers custom symbol authoring workflow, template layers, Draw annotation with guide points
---
## Part 1: Rendering Mode Decision Tree
SF Symbols support 4 rendering modes. The right choice depends on your design intent.
### Quick Decision
```
Need depth from ONE color? → Hierarchical
Need specific colors per layer? → Palette
Want Apple's curated colors? → Multicolor
Just need a tinted icon? → Monochrome (default)
```
### Monochrome
The default mode. Every layer renders in the same color (your `foregroundStyle`).
```swift
Image(systemName: "cloud.rain.fill")
.foregroundStyle(.blue)
// All layers are blue
```
**When to use**: Simple tinted icons, matching text color, toolbar items, tab bar items.
### Hierarchical
Renders layers at different opacities derived from a **single** color. Primary layers are fully opaque; secondary and tertiary layers get progressively more transparent. Creates depth without specifying multiple colors.
```swift
Image(systemName: "cloud.rain.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
// Cloud is full blue, rain drops are lighter blue
```
**When to use**: When you want visual depth but still want the icon to feel cohesive with a single hue. Most common choice for polished UI.
### Palette
Each layer gets an **explicit** color. Unlike Hierarchical, no automatic opacity derivation — you control each layer's color directly.
```swift
Image(systemName: "cloud.rain.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.blue, .cyan)
// Cloud is blue, rain drops are cyan
```
**When to use**: Branded icons, status indicators where specific colors carry meaning, designs requiring exact color control.
**Gotcha**: If you provide fewer colors than layers, extra layers reuse the last color. If the symbol has 3 layers and you provide 2 colors, the third layer uses the second color.
### Multicolor
Uses Apple's predefined color scheme for each symbol. Colors are fixed — you cannot customize them.
```swift
Image(systemName: "cloud.rain.fill")
.symbolRenderingMode(.multicolor)
// Cloud is white, rain drops are blue (Apple's design)
```
**When to use**: Weather indicators, file type icons, or anywhere Apple's curated design intent matches your needs. Not all symbols support Multicolor — unsupported symbols fall back to Monochrome.
### Common Mistakes
| Mistake | Impact | Fix |
|---------|--------|-----|
| Using `.foregroundColor()` with Multicolor | Overrides Apple's colors | Remove foreground color modifier |
| Setting Palette with only 1 color | Looks like Monochrome | Provide colors for each layer |
| Assuming all symbols support Multicolor | Fallback to Monochrome | Check in SF Symbols app first |
| Using Hierarchical when layers need distinct meanings | Colors don't carry semantic intent | Use Palette instead |
---
## Part 2: Symbol Effects System
Symbol effects bring SF Symbols to life with motion. Every effect falls into one of four behavioral categories.
### Effect Categories
| Category | Trigger | Duration | Use Case |
|----------|---------|----------|----------|
| **Discrete** | Value change | One-shot | Tap feedback, event notification |
| **Indefinite** | `isActive` bool | Continuous until stopped | Loading states, ongoing processes |
| **Transition** | View insert/remove | One-shot | Appear/disappear with style |
| **Content Transition** | Symbol swap | One-shot | Replacing one symbol with another |
### Which Effect for Which UX Purpose
```
User tapped something → Bounce (discrete)
Something changed, draw attention → Wiggle (discrete, iOS 18+)
Ongoing process/loading → Pulse, Breathe, or Variable Color (indefinite)
Rotation indicates progress → Rotate (indefinite, iOS 18+)
Show/hide symbol → Appear/Disappear (transition)
Swap between two symbols → Replace (content transition)
Symbol enters with hand-drawn style → Draw On (iOS 26+)
Symbol exits with hand-drawn style → Draw Off (iOS 26+)
Progress indicator along path → Variable Draw (iOS 26+)
Scale up/down for emphasis → Scale (indefinite)
```
### Discrete Effects
Fire once when a value changes. The symbol performs the animation and returns to its resting state.
#### Bounce
The most common discrete effect. A brief, springy animation.
```swift
@State private var downloadCount = 0
Image(systemName: "arrow.down.circle")
.symbolEffect(.bounce, value: downloadCount)
```
The animation triggers each time `downloadCount` changes.
**Directional options**: `.bounce.up`, `.bounce.down`
#### Wiggle (iOS 18+)
A horizontal shake that draws attention to the symbol.
```swift
Image(systemName: "bell.fill")
.symbolEffect(.wiggle, value: notificationCount)
```
**Directional options**: `.wiggle.left`, `.wiggle.right`, `.wiggle.forward`, `.wiggle.backward`
`.forward` and `.backward` respect reading direction — use these for RTL support.
#### Rotate (as Discrete, iOS 18+)
A single rotation when triggered by value change.
```swift
Image(systemName: "arrow.trianglehead.2.clockwise")
.symbolEffect(.rotate, value: refreshCount)
```
**Options**: `.rotate.clockwise`, `.rotate.counterClockwise`
**By Layer**: Some symbols rotate only specific layers (e.g., fan blades spin but the housing stays fixed). Use `.rotate.byLayer` to activate this.
### Indefinite Effects
Run continuously while `isActive` is `true`. Stop when `isActive` becomes `false`.
#### Pulse
A subtle opacity pulse. Good for "waiting" states.
```swift
Image(systemName: "network")
.symbolEffect(.pulse, isActive: isConnecting)
```
#### Variable Color
Iterates through the symbol's layers, highlighting each in sequence. Creates a "filling up" or "cycling" look.
```swift
Image(systemName: "wifi")
.symbolEffect(.variableColor.iterative, isActive: isSearching)
```
**Variants**:
- `.variableColor.iterative` — highlights one layer at a time
- `.variableColor.cumulative` — progressively fills layers
- `.variableColor.reversing` — cycles back and forth
- Combine: `.variableColor.iterative.reversing`
#### Scale
Scales the symbol up or down.
```swift
Image(systemName: "mic.fill")
.symbolEffect(.scale.up, isActive: isRecording)
```
#### Breathe (iOS 18+)
A smooth, rhythmic scale animation — like the symbol is breathing.
```swift
Image(systemName: "heart.fill")
.symbolEffect(.breathe, isActive: isMonitoring)
```
**Variants**: `.breathe.plain` (scale only), `.breathe.pulse` (scale + opacity)
#### Rotate (as Indefinite, iOS 18+)
Continuous rotation for processing indicators.
```swift
Image(systemName: "gear")
.symbolEffect(.rotate, isActive: isProcessing)
```
### Effect Options
All effects accept `SymbolEffectOptions` via the `options` parameter.
```swift
// Repeat 3 times
.symbolEffect(.bounce, options: .repeat(3), value: count)
// Double speed
.symbolEffect(.pulse, options: .speed(2.0), isActive: true)
// Repeat continuously
.symbolEffect(.variableColor, options: .repeat(.continuous), isActive: true)
// Non-repeating (run once)
.symbolEffect(.breathe, options: .nonRepeating, isActive: true)
// Combine options
.symbolEffect(.bounce, options: .repeat(5).speed(1.5), value: count)
```
### Transition Effects
Used when a symbol-based view appears or disappears from the view hierarchy.
```swift
if showSymbol {
Image(systemName: "checkmark.circle.fill")
.transition(.symbolEffect(.appear))
}
```
**Available transitions**: `.appear`, `.disappear`
**Variants**: `.appear.up`, `.appear.down`, `.disappear.up`, `.disappear.down`
### Content Transitions
Used to animate from one symbol to another. Applied to the container, not the symbol.
```swift
@State private var isFavorite = false
Button {
isFavorite.toggle()
} label: {
Image(systemName: isFavorite ? "star.fill" : "star")
.contentTransition(.symbolEffect(.replace))
}
```
**Replace variants**:
- `.replace.downUp` — old symbol moves down, new moves up
- `.replace.upUp` — both move up
- `.replace.offUp` — old fades off, new moves up
#### Magic Replace
When two symbols share a common structure (like `star` and `star.fill`, or `pause.fill` and `play.fill`), Replace automatically performs a **Magic Replace** — morphing shared elements while transitioning differing parts. Magic Replace is the default behavior for `.replace` in iOS 18+. For explicit control:
```swift
// Explicit Magic Replace with fallback
.contentTransition(.symbolEffect(.replace.magic(fallback: .replace.downUp)))
```
---
## Part 3: SF Symbols 7 — Draw Animations (iOS 26+)
Draw animations simulate the natural flow of drawing a symbol with a pen. This is the signature new feature in SF Symbols 7.
### Draw On and Draw Off
**Draw On** animates a symbol appearing by "drawing" it stroke by stroke.
**Draw Off** animates a symbol disappearing by "erasing" it.
```swift
// Draw On symbol draws in when isComplete becomes true
Image(systemName: "checkmark.circle")
.symbolEffect(.drawOn, isActive: isComplete)
// Draw Off symbol draws out when isHidden becomes true
Image(systemName: "star.fill")
.symbolEffect(.drawOff, isActive: isHidden)
```
### Playback Modes
Control how multi-layer symbols animate their draw:
```swift
// By Layer (default) staggered timing, layers overlap
Image(systemName: "square.and.arrow.up")
.symbolEffect(.drawOn.byLayer, isActive: showIcon)
// Whole Symbol all layers draw simultaneously
Image(systemName: "square.and.arrow.up")
.symbolEffect(.drawOn.wholeSymbol, isActive: showIcon)
// Individually sequential, each layer completes before next starts
Image(systemName: "square.and.arrow.up")
.symbolEffect(.drawOn.individually, isActive: showIcon)
```
**When to use each mode**:
- **By Layer** (default): Most natural feel, good for most symbols
- **Whole Symbol**: When the symbol should appear as one unit, not in parts
- **Individually**: When you want to emphasize each layer separately (storytelling, onboarding)
### Draw Off Direction
Draw Off supports controlling whether the animation plays forward or in reverse:
```swift
// Forward (default) follows the draw path
.symbolEffect(.drawOff.nonReversed, isActive: isHidden)
// Reversed erases in reverse order of how it was drawn
.symbolEffect(.drawOff.reversed, isActive: isErasing)
```
### Variable Draw
Variable Draw uses `SymbolVariableValueMode.draw` to partially draw a symbol's stroke path based on a 0.0 to 1.0 value — perfect for progress indicators.
```swift
Image(systemName: "thermometer.high", variableValue: temperature)
.symbolVariableValueMode(.draw) // iOS 26+
```
Compare with traditional Variable Color (which sets opacity per layer):
```swift
Image(systemName: "wifi", variableValue: signalStrength)
.symbolVariableValueMode(.color) // iOS 17+ (default behavior)
```
**Constraint**: A symbol can support both Variable Color and Variable Draw, but only one mode can be active at render time. Setting an unsupported mode has no visible effect.
### Gradient Rendering
SF Symbols 7 introduces `SymbolColorRenderingMode` for gradient fills generated from a single source color.
```swift
Image(systemName: "star.fill")
.symbolColorRenderingMode(.gradient) // iOS 26+
.foregroundStyle(.red)
```
| Mode | Description |
|------|-------------|
| `.flat` | Solid color fill (default) |
| `.gradient` | Axial gradient from source color |
Gradients work with all rendering modes and are most effective at larger sizes.
### Magic Replace with Draw
When using `.contentTransition(.symbolEffect(.replace))` between certain symbol pairs, the system now combines Draw Off on the outgoing symbol with Draw On for the incoming symbol. The enclosure (if shared, like a circle outline) is preserved while inner elements transition with draw animations.
```swift
// Automatic Draw-enhanced Magic Replace
Image(systemName: isComplete ? "checkmark.circle.fill" : "circle")
.contentTransition(.symbolEffect(.replace))
```
### Custom Symbol Draw Annotation
To enable Draw animations on custom symbols, annotate paths in the SF Symbols app:
1. **Open** your custom symbol in SF Symbols 7
2. **Select** a path layer
3. **Add guide points** to define draw direction:
- **Start point** (open circle): Where drawing begins
- **End point** (closed circle): Where drawing ends
- **Corner point** (diamond): Sharp direction changes
- **Bidirectional point**: Enables center-outward drawing
- **Attachment point**: Connects non-drawing decorative elements
4. **Minimum**: Two guide points per path (start and end)
5. **Test** using the Preview panel in SF Symbols app
**Option-drag** guide points for precise placement. Use context menus to configure direction and end caps.
---
## Part 4: Anti-Patterns
### Wrong Rendering Mode
| Pattern | Problem | Fix |
|---------|---------|-----|
| Palette with 1 color | Equivalent to Monochrome, wasted API call | Use Monochrome or provide multiple colors |
| Multicolor for branded icons | Can't customize Apple's fixed colors | Use Palette with brand colors |
| Hardcoded `.foregroundColor(.blue)` | Ignores Dark Mode, Dynamic Type, accessibility | Use `.foregroundStyle()` with semantic colors |
| Hierarchical for status indicators | Layers don't carry distinct meaning | Use Palette with semantic colors |
### Wrong Effect Choice
| Pattern | Problem | Fix |
|---------|---------|-----|
| Bounce for loading state | One-shot, doesn't convey "ongoing" | Use Pulse, Breathe, or Variable Color |
| Pulse for tap feedback | Too subtle for confirming action | Use Bounce |
| Continuous Rotate for non-mechanical symbols | Looks unnatural for organic shapes | Use Breathe for organic symbols |
| Draw On for transient state changes | Too dramatic for frequent toggles | Use Replace or Scale |
### Missing iOS Version Checks
```swift
// Crashes on iOS 17
Image(systemName: "bell")
.symbolEffect(.wiggle, value: count) // Wiggle requires iOS 18+
// Safe version check
Image(systemName: "bell")
.modifier(BellEffectModifier(count: count))
struct BellEffectModifier: ViewModifier {
let count: Int
func body(content: Content) -> some View {
if #available(iOS 18, *) {
content.symbolEffect(.wiggle, value: count)
} else {
content.symbolEffect(.bounce, value: count)
}
}
}
```
### Ignoring Reduce Motion
Symbol effects **automatically** respect the Reduce Motion accessibility setting — most effects are suppressed or simplified. However, if you're using effects to convey essential information (not just decoration), provide an alternative:
```swift
// Variable Color conveys WiFi strength provide text fallback
Image(systemName: "wifi")
.symbolEffect(.variableColor, isActive: isSearching)
.accessibilityLabel("Searching for WiFi networks")
```
**Do not** disable Reduce Motion or try to force-play effects. The system handles this correctly.
### Missing Accessibility Labels
```swift
// VoiceOver says "star.fill"
Image(systemName: "star.fill")
// VoiceOver says "Favorite"
Image(systemName: "star.fill")
.accessibilityLabel("Favorite")
```
When using `.contentTransition(.symbolEffect(.replace))` to swap symbols, update the accessibility label to match the current state:
```swift
Image(systemName: isFavorite ? "star.fill" : "star")
.contentTransition(.symbolEffect(.replace))
.accessibilityLabel(isFavorite ? "Remove from favorites" : "Add to favorites")
```
---
## Part 5: Troubleshooting
### Effect Not Playing
**Symptom**: `.symbolEffect()` modifier applied but no animation visible.
1. **Check iOS version** — Bounce/Pulse/Scale require iOS 17+, Wiggle/Rotate/Breathe require iOS 18+, Draw requires iOS 26+
2. **Check Reduce Motion** — Settings > Accessibility > Motion > Reduce Motion. If on, most effects are suppressed
3. **Check trigger type** — Discrete effects need `value:` that changes. Indefinite effects need `isActive: true`. Transition effects need the view to actually enter/leave the hierarchy
4. **Check symbol compatibility** — Not all symbols support all effects. Open the SF Symbols app, select the symbol, and check the Animation inspector
5. **Check for conflicting effects** — Multiple `.symbolEffect()` modifiers on the same view can conflict. Use a single effect or combine with options
### Wrong Colors in Rendering Mode
**Symptom**: Symbol colors don't match expected appearance.
1. **Check rendering mode** — If you set `.foregroundStyle` but see only one color, you may need `.symbolRenderingMode(.palette)` or `.hierarchical`
2. **Check `.tint` vs `.foregroundStyle`** — In UIKit, `tintColor` affects Monochrome and Hierarchical. For Palette, use `UIImage.SymbolConfiguration(paletteColors:)`
3. **Check Multicolor support** — Not all symbols have Multicolor variants. Unsupported symbols fall back to Monochrome
4. **Check environment**`.foregroundStyle` from a parent view may override your rendering mode. Apply `.symbolRenderingMode()` directly on the Image
### Custom Symbol Weight Mismatch
**Symptom**: Custom symbol looks too thin or too thick next to text or other symbols.
1. **Check template weight** — Custom symbols need weight variants matching the 9 SF Pro weights. Export from SF Symbols app handles this
2. **Check `.font()` alignment** — The symbol's weight follows the applied font weight. If using `.font(.title)`, ensure your custom symbol has appropriate weight variants
3. **Check scale**`.imageScale(.small/.medium/.large)` affects overall size. Use `.font()` for weight matching
### Draw Animation Not Working on Custom Symbol
**Symptom**: `.symbolEffect(.drawOn)` applied to custom symbol but no draw animation occurs.
1. **Check guide points** — Custom symbols need Draw annotation with at least 2 guide points per path (start + end)
2. **Check SF Symbols app version** — Draw annotation requires SF Symbols 7+
3. **Check path structure** — Guide points must be placed on stroked paths, not fills. Convert fills to strokes where draw animation is desired
4. **Check layer structure** — Each annotatable layer needs its own guide points
---
## Part 6: Pressure Scenarios
### Scenario 1: "Just use a static image, symbols are overkill"
**Setup**: Designer provides PNG icons. Developer considers using them instead of SF Symbols.
**Why this matters**: Static PNGs don't adapt to Dynamic Type, Bold Text, Dark Mode, or accessibility settings. They also don't support symbol effects.
**Professional response**: "SF Symbols scale with text, support 9 weights, adapt to Dark Mode and Bold Text automatically, and enable animations without custom code. A PNG requires @1x/@2x/@3x variants, manual Dark Mode handling, manual Dynamic Type scaling, and custom animation code. The 10 minutes to find the right SF Symbol saves hours of asset management."
**Time cost of skipping**: 2-4 hours managing assets + ongoing maintenance vs 10 minutes finding the right symbol.
### Scenario 2: "We'll add animations later"
**Setup**: Sprint deadline. PM says animations are polish and can wait.
**Why this matters**: Retrofitting symbol effects requires restructuring state management. Effects triggered by `value:` changes need the right state architecture from the start.
**Professional response**: "Adding `.symbolEffect(.bounce, value: count)` takes one line. Retrofitting the state to support it later takes a refactor. Let me add the effect now — it's literally one modifier."
### Scenario 3: "Draw animations look janky on our custom symbols"
**Setup**: Custom symbols have Draw animations that look wrong — paths draw in unexpected order or direction.
**Why this matters**: Draw annotation requires intentional guide point placement. Without it, the system guesses and often gets it wrong.
**Fix**: Open custom symbols in SF Symbols 7 app, add guide points explicitly to each path defining start/end/direction. Test each weight variant. See Custom Symbol Draw Annotation section above.
---
## Resources
**WWDC**: 2023-10257, 2023-10258, 2024-10188, 2025-337
**Docs**: /symbols, /symbols/symboleffect, /symbols/symbolrenderingmode, /swiftui/image/symboleffect(_:options:value:), /swiftui/image/symbolrenderingmode(_:)
**Skills**: axiom-sf-symbols-ref, axiom-hig-ref, axiom-swiftui-animation-ref
---
**Last Updated** Based on WWDC 2023/10257-10258, WWDC 2024/10188, WWDC 2025/337
**Version** iOS 17+ (effects), iOS 18+ (Wiggle/Rotate/Breathe), iOS 26+ (Draw On/Off, Variable Draw, Gradients)