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.
This commit is contained in:
976
.claude/skills/axiom-sf-symbols-ref/SKILL.md
Normal file
976
.claude/skills/axiom-sf-symbols-ref/SKILL.md
Normal file
@@ -0,0 +1,976 @@
|
||||
---
|
||||
name: axiom-sf-symbols-ref
|
||||
description: Use when you need complete SF Symbols API reference including every rendering mode, symbol effect, configuration option, UIKit equivalent, and platform availability - comprehensive code examples for iOS 17 through iOS 26
|
||||
license: MIT
|
||||
compatibility: iOS 17+, iOS 18+, iOS 26+
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# SF Symbols — API Reference
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use when:
|
||||
- You need exact API signatures for rendering modes or symbol effects
|
||||
- You need UIKit/AppKit equivalents for SwiftUI symbol APIs
|
||||
- You need to check platform availability for a specific effect
|
||||
- You need configuration options (weight, scale, variable values)
|
||||
- You need to create custom symbols with proper template structure
|
||||
|
||||
#### Related Skills
|
||||
- Use `axiom-sf-symbols` for decision trees, anti-patterns, troubleshooting, and when to use which effect
|
||||
- Use `axiom-swiftui-animation-ref` for general SwiftUI animation (non-symbol)
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Symbol Display
|
||||
|
||||
### SwiftUI
|
||||
|
||||
```swift
|
||||
// Basic display
|
||||
Image(systemName: "star.fill")
|
||||
|
||||
// With Label (icon + text)
|
||||
Label("Favorites", systemImage: "star.fill")
|
||||
|
||||
// Font sizing — symbol scales with text
|
||||
Image(systemName: "star.fill")
|
||||
.font(.title)
|
||||
|
||||
// Image scale — relative sizing without changing font
|
||||
Image(systemName: "star.fill")
|
||||
.imageScale(.large) // .small, .medium, .large
|
||||
|
||||
// Explicit point size
|
||||
Image(systemName: "star.fill")
|
||||
.font(.system(size: 24))
|
||||
|
||||
// Weight — matches SF Pro font weights
|
||||
Image(systemName: "star.fill")
|
||||
.fontWeight(.bold) // .ultraLight through .black
|
||||
|
||||
// Symbol variant — programmatic .fill, .circle, .square, .slash
|
||||
Image(systemName: "person")
|
||||
.symbolVariant(.circle.fill) // Renders person.circle.fill
|
||||
|
||||
// Variable value — 0.0 to 1.0, controls symbol fill level
|
||||
Image(systemName: "speaker.wave.3.fill", variableValue: 0.5)
|
||||
```
|
||||
|
||||
### UIKit
|
||||
|
||||
```swift
|
||||
// Basic display
|
||||
let image = UIImage(systemName: "star.fill")
|
||||
imageView.image = image
|
||||
|
||||
// Configuration — point size and weight
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 24, weight: .bold)
|
||||
let image = UIImage(systemName: "star.fill", withConfiguration: config)
|
||||
|
||||
// Configuration — text style (scales with Dynamic Type)
|
||||
let config = UIImage.SymbolConfiguration(textStyle: .title1)
|
||||
let image = UIImage(systemName: "star.fill", withConfiguration: config)
|
||||
|
||||
// Configuration — scale
|
||||
let config = UIImage.SymbolConfiguration(scale: .large) // .small, .medium, .large
|
||||
|
||||
// Combine configurations
|
||||
let sizeConfig = UIImage.SymbolConfiguration(pointSize: 24, weight: .bold, scale: .large)
|
||||
|
||||
// Variable value
|
||||
let image = UIImage(systemName: "speaker.wave.3.fill", variableValue: 0.5)
|
||||
```
|
||||
|
||||
### AppKit
|
||||
|
||||
```swift
|
||||
// Basic display
|
||||
let image = NSImage(systemSymbolName: "star.fill", accessibilityDescription: "Favorite")
|
||||
|
||||
// Configuration
|
||||
let config = NSImage.SymbolConfiguration(pointSize: 24, weight: .bold)
|
||||
let configured = image?.withSymbolConfiguration(config)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Rendering Modes
|
||||
|
||||
### SwiftUI
|
||||
|
||||
```swift
|
||||
// Monochrome (default)
|
||||
Image(systemName: "cloud.rain.fill")
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
// Hierarchical — depth from single color
|
||||
Image(systemName: "cloud.rain.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.blue)
|
||||
|
||||
// Palette — explicit color per layer
|
||||
Image(systemName: "cloud.rain.fill")
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .blue)
|
||||
// For 3-layer symbols:
|
||||
.foregroundStyle(.red, .white, .blue)
|
||||
|
||||
// Multicolor — Apple's curated colors
|
||||
Image(systemName: "cloud.rain.fill")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
|
||||
// Preferred rendering mode — uses symbol's preferred mode
|
||||
// Falls back gracefully if the symbol doesn't support it
|
||||
Image(systemName: "cloud.rain.fill")
|
||||
.symbolRenderingMode(.monochrome) // explicit monochrome
|
||||
```
|
||||
|
||||
#### SymbolRenderingMode Enum
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `.monochrome` | Single color for all layers (default) |
|
||||
| `.hierarchical` | Single color with automatic opacity per layer |
|
||||
| `.palette` | Explicit color per layer via `.foregroundStyle()` |
|
||||
| `.multicolor` | Apple's fixed curated colors |
|
||||
|
||||
### UIKit
|
||||
|
||||
```swift
|
||||
// Hierarchical
|
||||
let config = UIImage.SymbolConfiguration(hierarchicalColor: .systemBlue)
|
||||
imageView.preferredSymbolConfiguration = config
|
||||
|
||||
// Palette
|
||||
let config = UIImage.SymbolConfiguration(paletteColors: [.white, .systemBlue])
|
||||
imageView.preferredSymbolConfiguration = config
|
||||
|
||||
// Multicolor
|
||||
let config = UIImage.SymbolConfiguration.preferringMulticolor()
|
||||
imageView.preferredSymbolConfiguration = config
|
||||
|
||||
// Monochrome — just set tintColor
|
||||
imageView.tintColor = .systemBlue
|
||||
```
|
||||
|
||||
### Combining Configurations (UIKit)
|
||||
|
||||
```swift
|
||||
let sizeConfig = UIImage.SymbolConfiguration(pointSize: 24, weight: .bold)
|
||||
let colorConfig = UIImage.SymbolConfiguration(paletteColors: [.white, .blue, .gray])
|
||||
let combined = sizeConfig.applying(colorConfig)
|
||||
imageView.preferredSymbolConfiguration = combined
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Symbol Effects — Complete API
|
||||
|
||||
### Effect Protocol Hierarchy
|
||||
|
||||
All symbol effects conform to `SymbolEffect`. Sub-protocols define behavior:
|
||||
|
||||
| Protocol | Trigger | Modifier | Loop |
|
||||
|----------|---------|----------|------|
|
||||
| `DiscreteSymbolEffect` | `value:` (Equatable) | `.symbolEffect(_:options:value:)` | No |
|
||||
| `IndefiniteSymbolEffect` | `isActive:` (Bool) | `.symbolEffect(_:options:isActive:)` | Yes |
|
||||
| `TransitionSymbolEffect` | View lifecycle | `.transition(.symbolEffect(_:))` | No |
|
||||
| `ContentTransitionSymbolEffect` | Symbol change | `.contentTransition(.symbolEffect(_:))` | No |
|
||||
|
||||
### Remove All Effects (SwiftUI)
|
||||
|
||||
```swift
|
||||
// Strip all symbol effects from a view hierarchy
|
||||
Image(systemName: "star.fill")
|
||||
.symbolEffectsRemoved() // Removes all effects
|
||||
.symbolEffectsRemoved(false) // Re-enables effects
|
||||
```
|
||||
|
||||
### SymbolEffectOptions
|
||||
|
||||
```swift
|
||||
// Speed multiplier
|
||||
.symbolEffect(.bounce, options: .speed(2.0), value: count)
|
||||
|
||||
// Repeat count
|
||||
.symbolEffect(.bounce, options: .repeat(3), value: count)
|
||||
|
||||
// Continuous repeat
|
||||
.symbolEffect(.pulse, options: .repeat(.continuous), isActive: true)
|
||||
|
||||
// Non-repeating (for indefinite effects, run once then hold)
|
||||
.symbolEffect(.breathe, options: .nonRepeating, isActive: true)
|
||||
|
||||
// Combined
|
||||
.symbolEffect(.wiggle, options: .repeat(5).speed(1.5), value: count)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bounce
|
||||
|
||||
**Protocols**: `DiscreteSymbolEffect`
|
||||
|
||||
```swift
|
||||
// Discrete — triggers on value change
|
||||
Image(systemName: "arrow.down.circle")
|
||||
.symbolEffect(.bounce, value: downloadCount)
|
||||
|
||||
// Directional
|
||||
.symbolEffect(.bounce.up, value: count)
|
||||
.symbolEffect(.bounce.down, value: count)
|
||||
|
||||
// By Layer — different layers bounce at different times
|
||||
.symbolEffect(.bounce.byLayer, value: count)
|
||||
|
||||
// Whole Symbol — entire symbol bounces together
|
||||
.symbolEffect(.bounce.wholeSymbol, value: count)
|
||||
```
|
||||
|
||||
**UIKit**:
|
||||
```swift
|
||||
imageView.addSymbolEffect(.bounce)
|
||||
// With options:
|
||||
imageView.addSymbolEffect(.bounce, options: .repeat(3))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pulse
|
||||
|
||||
**Protocols**: `DiscreteSymbolEffect`, `IndefiniteSymbolEffect`
|
||||
|
||||
```swift
|
||||
// Indefinite — continuous while active
|
||||
Image(systemName: "network")
|
||||
.symbolEffect(.pulse, isActive: isConnecting)
|
||||
|
||||
// Discrete — triggers once on value change
|
||||
.symbolEffect(.pulse, value: errorCount)
|
||||
|
||||
// By Layer
|
||||
.symbolEffect(.pulse.byLayer, isActive: true)
|
||||
|
||||
// Whole Symbol
|
||||
.symbolEffect(.pulse.wholeSymbol, isActive: true)
|
||||
```
|
||||
|
||||
**UIKit**:
|
||||
```swift
|
||||
imageView.addSymbolEffect(.pulse)
|
||||
imageView.removeSymbolEffect(ofType: PulseSymbolEffect.self)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Variable Color
|
||||
|
||||
**Protocols**: `DiscreteSymbolEffect`, `IndefiniteSymbolEffect`
|
||||
|
||||
```swift
|
||||
// Iterative — highlights one layer at a time
|
||||
Image(systemName: "wifi")
|
||||
.symbolEffect(.variableColor.iterative, isActive: isSearching)
|
||||
|
||||
// Cumulative — progressively fills layers
|
||||
.symbolEffect(.variableColor.cumulative, isActive: true)
|
||||
|
||||
// Reversing — cycles back and forth
|
||||
.symbolEffect(.variableColor.iterative.reversing, isActive: true)
|
||||
|
||||
// Hide inactive layers (dims non-highlighted layers)
|
||||
.symbolEffect(.variableColor.iterative.hideInactiveLayers, isActive: true)
|
||||
|
||||
// Dim inactive layers (slightly reduces opacity of non-highlighted)
|
||||
.symbolEffect(.variableColor.iterative.dimInactiveLayers, isActive: true)
|
||||
```
|
||||
|
||||
**UIKit**:
|
||||
```swift
|
||||
imageView.addSymbolEffect(.variableColor.iterative)
|
||||
imageView.removeSymbolEffect(ofType: VariableColorSymbolEffect.self)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scale
|
||||
|
||||
**Protocols**: `IndefiniteSymbolEffect`
|
||||
|
||||
```swift
|
||||
// Scale up
|
||||
Image(systemName: "mic.fill")
|
||||
.symbolEffect(.scale.up, isActive: isRecording)
|
||||
|
||||
// Scale down
|
||||
.symbolEffect(.scale.down, isActive: isMuted)
|
||||
|
||||
// By Layer
|
||||
.symbolEffect(.scale.up.byLayer, isActive: true)
|
||||
|
||||
// Whole Symbol
|
||||
.symbolEffect(.scale.up.wholeSymbol, isActive: true)
|
||||
```
|
||||
|
||||
**UIKit**:
|
||||
```swift
|
||||
imageView.addSymbolEffect(.scale.up)
|
||||
imageView.removeSymbolEffect(ofType: ScaleSymbolEffect.self)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Wiggle (iOS 18+)
|
||||
|
||||
**Protocols**: `DiscreteSymbolEffect`, `IndefiniteSymbolEffect`
|
||||
|
||||
```swift
|
||||
// Discrete
|
||||
Image(systemName: "bell.fill")
|
||||
.symbolEffect(.wiggle, value: notificationCount)
|
||||
|
||||
// Directional
|
||||
.symbolEffect(.wiggle.left, value: count)
|
||||
.symbolEffect(.wiggle.right, value: count)
|
||||
.symbolEffect(.wiggle.forward, value: count) // RTL-aware
|
||||
.symbolEffect(.wiggle.backward, value: count) // RTL-aware
|
||||
.symbolEffect(.wiggle.up, value: count)
|
||||
.symbolEffect(.wiggle.down, value: count)
|
||||
.symbolEffect(.wiggle.clockwise, value: count)
|
||||
.symbolEffect(.wiggle.counterClockwise, value: count)
|
||||
|
||||
// Custom angle
|
||||
.symbolEffect(.wiggle.custom(angle: .degrees(15)), value: count)
|
||||
|
||||
// By Layer
|
||||
.symbolEffect(.wiggle.byLayer, value: count)
|
||||
```
|
||||
|
||||
**UIKit**:
|
||||
```swift
|
||||
imageView.addSymbolEffect(.wiggle)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Rotate (iOS 18+)
|
||||
|
||||
**Protocols**: `DiscreteSymbolEffect`, `IndefiniteSymbolEffect`
|
||||
|
||||
```swift
|
||||
// Indefinite rotation
|
||||
Image(systemName: "gear")
|
||||
.symbolEffect(.rotate, isActive: isProcessing)
|
||||
|
||||
// Direction
|
||||
.symbolEffect(.rotate.clockwise, isActive: true)
|
||||
.symbolEffect(.rotate.counterClockwise, isActive: true)
|
||||
|
||||
// By Layer — only specific layers rotate (e.g., fan blades)
|
||||
.symbolEffect(.rotate.byLayer, isActive: true)
|
||||
```
|
||||
|
||||
**UIKit**:
|
||||
```swift
|
||||
imageView.addSymbolEffect(.rotate)
|
||||
imageView.removeSymbolEffect(ofType: RotateSymbolEffect.self)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Breathe (iOS 18+)
|
||||
|
||||
**Protocols**: `DiscreteSymbolEffect`, `IndefiniteSymbolEffect`
|
||||
|
||||
```swift
|
||||
// Basic breathe
|
||||
Image(systemName: "heart.fill")
|
||||
.symbolEffect(.breathe, isActive: isMonitoring)
|
||||
|
||||
// Plain — scale only
|
||||
.symbolEffect(.breathe.plain, isActive: true)
|
||||
|
||||
// Pulse — scale + opacity variation
|
||||
.symbolEffect(.breathe.pulse, isActive: true)
|
||||
|
||||
// By Layer
|
||||
.symbolEffect(.breathe.byLayer, isActive: true)
|
||||
```
|
||||
|
||||
**UIKit**:
|
||||
```swift
|
||||
imageView.addSymbolEffect(.breathe)
|
||||
imageView.removeSymbolEffect(ofType: BreatheSymbolEffect.self)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Appear and Disappear
|
||||
|
||||
**Protocols**: `TransitionSymbolEffect`
|
||||
|
||||
```swift
|
||||
// SwiftUI transition
|
||||
if showSymbol {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.transition(.symbolEffect(.appear))
|
||||
}
|
||||
|
||||
if showSymbol {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.transition(.symbolEffect(.disappear))
|
||||
}
|
||||
|
||||
// Directional
|
||||
.transition(.symbolEffect(.appear.up))
|
||||
.transition(.symbolEffect(.appear.down))
|
||||
.transition(.symbolEffect(.disappear.up))
|
||||
.transition(.symbolEffect(.disappear.down))
|
||||
|
||||
// By Layer
|
||||
.transition(.symbolEffect(.appear.byLayer))
|
||||
|
||||
// Whole Symbol
|
||||
.transition(.symbolEffect(.appear.wholeSymbol))
|
||||
```
|
||||
|
||||
**UIKit** (as effect, not transition):
|
||||
```swift
|
||||
// Make symbol appear
|
||||
imageView.addSymbolEffect(.appear)
|
||||
|
||||
// Make symbol disappear
|
||||
imageView.addSymbolEffect(.disappear)
|
||||
|
||||
// Appear after disappear
|
||||
imageView.addSymbolEffect(.appear) // re-shows hidden symbol
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Replace
|
||||
|
||||
**Protocols**: `ContentTransitionSymbolEffect`
|
||||
|
||||
```swift
|
||||
// SwiftUI content transition
|
||||
Image(systemName: isFavorite ? "star.fill" : "star")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
|
||||
// Directional variants
|
||||
.contentTransition(.symbolEffect(.replace.downUp))
|
||||
.contentTransition(.symbolEffect(.replace.upUp))
|
||||
.contentTransition(.symbolEffect(.replace.offUp))
|
||||
|
||||
// By Layer
|
||||
.contentTransition(.symbolEffect(.replace.byLayer))
|
||||
|
||||
// Whole Symbol
|
||||
.contentTransition(.symbolEffect(.replace.wholeSymbol))
|
||||
|
||||
// Magic Replace — default in iOS 18+, morphs shared elements
|
||||
// Automatic for structurally related pairs: star ↔ star.fill, pause.fill ↔ play.fill
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
|
||||
// Explicit Magic Replace with fallback for unrelated symbols
|
||||
.contentTransition(.symbolEffect(.replace.magic(fallback: .replace.downUp)))
|
||||
```
|
||||
|
||||
**UIKit**:
|
||||
```swift
|
||||
// Change symbol with Replace transition
|
||||
let newImage = UIImage(systemName: "star.fill")
|
||||
imageView.setSymbolImage(newImage!, contentTransition: .replace)
|
||||
|
||||
// Directional
|
||||
imageView.setSymbolImage(newImage!, contentTransition: .replace.downUp)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Draw Effects (iOS 26+)
|
||||
|
||||
### Draw On
|
||||
|
||||
```swift
|
||||
// Indefinite — draws in while active
|
||||
Image(systemName: "checkmark.circle")
|
||||
.symbolEffect(.drawOn, isActive: isComplete)
|
||||
|
||||
// Playback modes
|
||||
.symbolEffect(.drawOn.byLayer, isActive: isActive)
|
||||
.symbolEffect(.drawOn.wholeSymbol, isActive: isActive)
|
||||
.symbolEffect(.drawOn.individually, isActive: isActive)
|
||||
|
||||
// With options
|
||||
.symbolEffect(.drawOn, options: .speed(2.0), isActive: isActive)
|
||||
.symbolEffect(.drawOn, options: .nonRepeating, isActive: isActive)
|
||||
```
|
||||
|
||||
### Draw Off
|
||||
|
||||
```swift
|
||||
// Indefinite — draws out while active
|
||||
Image(systemName: "star.fill")
|
||||
.symbolEffect(.drawOff, isActive: isHidden)
|
||||
|
||||
// Playback modes
|
||||
.symbolEffect(.drawOff.byLayer, isActive: isActive)
|
||||
.symbolEffect(.drawOff.wholeSymbol, isActive: isActive)
|
||||
.symbolEffect(.drawOff.individually, isActive: isActive)
|
||||
|
||||
// Direction control
|
||||
.symbolEffect(.drawOff.nonReversed, isActive: isActive) // follows draw path forward
|
||||
.symbolEffect(.drawOff.reversed, isActive: isActive) // erases in reverse order
|
||||
```
|
||||
|
||||
### UIKit Draw Effects
|
||||
|
||||
```swift
|
||||
// Draw On
|
||||
imageView.addSymbolEffect(.drawOn)
|
||||
|
||||
// Draw Off
|
||||
imageView.addSymbolEffect(.drawOff)
|
||||
|
||||
// Remove
|
||||
imageView.removeSymbolEffect(ofType: DrawOnSymbolEffect.self)
|
||||
```
|
||||
|
||||
### Variable Draw
|
||||
|
||||
Uses `SymbolVariableValueMode` to control how variable values are rendered.
|
||||
|
||||
```swift
|
||||
// Variable Draw — draws stroke proportional to value (iOS 26+)
|
||||
Image(systemName: "thermometer.high", variableValue: temperature)
|
||||
.symbolVariableValueMode(.draw)
|
||||
|
||||
// Variable Color — sets layer opacity based on threshold (iOS 17+, default)
|
||||
Image(systemName: "wifi", variableValue: signalStrength)
|
||||
.symbolVariableValueMode(.color)
|
||||
```
|
||||
|
||||
#### SymbolVariableValueMode Enum (iOS 26+)
|
||||
|
||||
| Case | Description |
|
||||
|------|-------------|
|
||||
| `.color` | Sets opacity of each variable layer on/off based on threshold (existing behavior) |
|
||||
| `.draw` | Changes drawn length of each variable layer based on range |
|
||||
|
||||
**Constraint**: Some symbols support only one mode. Setting an unsupported mode has no visible effect. A symbol cannot use both Variable Color and Variable Draw simultaneously.
|
||||
|
||||
### Gradient Rendering (iOS 26+)
|
||||
|
||||
Uses `SymbolColorRenderingMode` for automatic gradient generation from a single color.
|
||||
|
||||
```swift
|
||||
// Gradient fill — system generates axial gradient from source color
|
||||
Image(systemName: "heart.fill")
|
||||
.symbolColorRenderingMode(.gradient)
|
||||
.foregroundStyle(.red)
|
||||
|
||||
// Works with any rendering mode
|
||||
Image(systemName: "cloud.rain.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.symbolColorRenderingMode(.gradient)
|
||||
.foregroundStyle(.blue)
|
||||
```
|
||||
|
||||
#### SymbolColorRenderingMode Enum (iOS 26+)
|
||||
|
||||
| Case | Description |
|
||||
|------|-------------|
|
||||
| `.flat` | Solid color fill (default) |
|
||||
| `.gradient` | Axial gradient generated from source color |
|
||||
|
||||
Gradients are most effective at larger symbol sizes and work across all rendering modes.
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Content Transition Patterns
|
||||
|
||||
### Symbol Swap with Replace
|
||||
|
||||
```swift
|
||||
struct PlayPauseButton: View {
|
||||
@State private var isPlaying = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
isPlaying.toggle()
|
||||
} label: {
|
||||
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
}
|
||||
.accessibilityLabel(isPlaying ? "Pause" : "Play")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Download Progress Pattern
|
||||
|
||||
```swift
|
||||
struct DownloadButton: View {
|
||||
@State private var state: DownloadState = .idle
|
||||
|
||||
var symbolName: String {
|
||||
switch state {
|
||||
case .idle: "arrow.down.circle"
|
||||
case .downloading: "stop.circle"
|
||||
case .complete: "checkmark.circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
advanceState()
|
||||
} label: {
|
||||
Image(systemName: symbolName)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.symbolEffect(.pulse, isActive: state == .downloading)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Toggle with Effect Feedback
|
||||
|
||||
```swift
|
||||
struct FavoriteButton: View {
|
||||
@Binding var isFavorite: Bool
|
||||
@State private var bounceValue = 0
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
isFavorite.toggle()
|
||||
bounceValue += 1
|
||||
} label: {
|
||||
Image(systemName: isFavorite ? "star.fill" : "star")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.symbolEffect(.bounce, value: bounceValue)
|
||||
.foregroundStyle(isFavorite ? .yellow : .gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Custom Symbols
|
||||
|
||||
### Template Structure
|
||||
|
||||
Custom symbols are SVG files with specific layer annotations:
|
||||
|
||||
1. **Export from design tool** as SVG
|
||||
2. **Import into SF Symbols app** (File > Import)
|
||||
3. **Set template type**: Monochrome, Hierarchical, Multicolor, or Variable Color
|
||||
4. **Annotate layers** for rendering modes:
|
||||
- **Primary** layer: Full opacity in Hierarchical
|
||||
- **Secondary** layer: Reduced opacity in Hierarchical
|
||||
- **Tertiary** layer: Most reduced opacity in Hierarchical
|
||||
5. **Set Palette colors** per layer if supporting Palette mode
|
||||
6. **Export** as `.svg` template for Xcode
|
||||
|
||||
### Draw Annotation (SF Symbols 7)
|
||||
|
||||
To enable Draw animations on custom symbols:
|
||||
|
||||
1. Select a path in SF Symbols 7 app
|
||||
2. Open the Draw annotation panel
|
||||
3. Place guide points on the path:
|
||||
|
||||
| Point Type | Visual | Purpose |
|
||||
|------------|--------|---------|
|
||||
| Start | Open circle | Where drawing begins |
|
||||
| End | Closed circle | Where drawing ends |
|
||||
| Corner | Diamond | Sharp direction change |
|
||||
| Bidirectional | Double arrow | Center-outward drawing |
|
||||
| Attachment | Link icon | Non-drawing decorative connection |
|
||||
|
||||
4. **Minimum**: 2 guide points per path (start + end)
|
||||
5. **Option-drag** for precise placement
|
||||
6. Test in Preview panel across all weights
|
||||
|
||||
### Weight Interpolation
|
||||
|
||||
Custom symbols should include designs for at least 3 weight variants:
|
||||
- **Ultralight** (thinnest)
|
||||
- **Regular** (middle)
|
||||
- **Black** (thickest)
|
||||
|
||||
The system interpolates between these for intermediate weights (Thin, Light, Medium, Semibold, Bold, Heavy).
|
||||
|
||||
### Importing to Xcode
|
||||
|
||||
1. In Xcode, open Asset Catalog
|
||||
2. Click **+** > **Symbol Image Set**
|
||||
3. Drag exported `.svg` from SF Symbols app
|
||||
4. Asset catalog symbols: `Image("custom.symbol.name")`. For symbols loaded from a bundle: `Image(systemName: "custom.symbol.name", bundle: .module)`
|
||||
|
||||
---
|
||||
|
||||
## Part 7: Platform Availability Matrix
|
||||
|
||||
### Rendering Modes
|
||||
|
||||
| Feature | iOS | macOS | watchOS | tvOS | visionOS |
|
||||
|---------|-----|-------|---------|------|----------|
|
||||
| Monochrome | 13+ | 11+ | 6+ | 13+ | 1+ |
|
||||
| Hierarchical | 15+ | 12+ | 8+ | 15+ | 1+ |
|
||||
| Palette | 15+ | 12+ | 8+ | 15+ | 1+ |
|
||||
| Multicolor | 15+ | 12+ | 8+ | 15+ | 1+ |
|
||||
| Variable Value | 16+ | 13+ | 9+ | 16+ | 1+ |
|
||||
|
||||
### Symbol Effects
|
||||
|
||||
| Effect | Category | iOS | macOS | watchOS | tvOS | visionOS |
|
||||
|--------|----------|-----|-------|---------|------|----------|
|
||||
| Bounce | Discrete | 17+ | 14+ | 10+ | 17+ | 1+ |
|
||||
| Pulse | Discrete/Indefinite | 17+ | 14+ | 10+ | 17+ | 1+ |
|
||||
| Variable Color | Discrete/Indefinite | 17+ | 14+ | 10+ | 17+ | 1+ |
|
||||
| Scale | Indefinite | 17+ | 14+ | 10+ | 17+ | 1+ |
|
||||
| Appear | Transition | 17+ | 14+ | 10+ | 17+ | 1+ |
|
||||
| Disappear | Transition | 17+ | 14+ | 10+ | 17+ | 1+ |
|
||||
| Replace | Content Transition | 17+ | 14+ | 10+ | 17+ | 1+ |
|
||||
| Wiggle | Discrete/Indefinite | 18+ | 15+ | 11+ | 18+ | 2+ |
|
||||
| Rotate | Discrete/Indefinite | 18+ | 15+ | 11+ | 18+ | 2+ |
|
||||
| Breathe | Discrete/Indefinite | 18+ | 15+ | 11+ | 18+ | 2+ |
|
||||
| Draw On | Indefinite | 26+ | Tahoe+ | 26+ | 26+ | 26+ |
|
||||
| Draw Off | Indefinite | 26+ | Tahoe+ | 26+ | 26+ | 26+ |
|
||||
| Variable Draw | Value-based | 26+ | Tahoe+ | 26+ | 26+ | 26+ |
|
||||
| Gradient Fill | Rendering | 26+ | Tahoe+ | 26+ | 26+ | 26+ |
|
||||
|
||||
### Effect Behavior Categories
|
||||
|
||||
| Category | What It Does | How to Trigger |
|
||||
|----------|-------------|----------------|
|
||||
| Discrete | One-shot animation, returns to rest | `.symbolEffect(_:value:)` — fires when value changes |
|
||||
| Indefinite | Loops while active | `.symbolEffect(_:isActive:)` — loops while `true` |
|
||||
| Transition | Plays on view insert/remove | `.transition(.symbolEffect(_:))` |
|
||||
| Content Transition | Plays when symbol changes | `.contentTransition(.symbolEffect(_:))` |
|
||||
|
||||
---
|
||||
|
||||
## Part 8: UIKit Complete Reference
|
||||
|
||||
### Adding Effects
|
||||
|
||||
```swift
|
||||
// Add indefinite effect
|
||||
imageView.addSymbolEffect(.pulse)
|
||||
imageView.addSymbolEffect(.breathe)
|
||||
imageView.addSymbolEffect(.rotate)
|
||||
imageView.addSymbolEffect(.variableColor.iterative)
|
||||
imageView.addSymbolEffect(.scale.up)
|
||||
|
||||
// Add with options
|
||||
imageView.addSymbolEffect(.bounce, options: .repeat(3))
|
||||
imageView.addSymbolEffect(.pulse, options: .speed(2.0))
|
||||
|
||||
// Add with completion handler
|
||||
imageView.addSymbolEffect(.bounce, options: .default) { context in
|
||||
// Called when effect finishes
|
||||
print("Bounce complete")
|
||||
}
|
||||
```
|
||||
|
||||
### Removing Effects
|
||||
|
||||
```swift
|
||||
// Remove specific effect type
|
||||
imageView.removeSymbolEffect(ofType: PulseSymbolEffect.self)
|
||||
imageView.removeSymbolEffect(ofType: ScaleSymbolEffect.self)
|
||||
imageView.removeSymbolEffect(ofType: RotateSymbolEffect.self)
|
||||
|
||||
// Remove all effects
|
||||
imageView.removeAllSymbolEffects()
|
||||
|
||||
// Remove with options
|
||||
imageView.removeSymbolEffect(ofType: PulseSymbolEffect.self, options: .default)
|
||||
|
||||
// Remove with completion
|
||||
imageView.removeSymbolEffect(ofType: PulseSymbolEffect.self) { context in
|
||||
print("Pulse removed")
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Symbol Images with Transitions
|
||||
|
||||
```swift
|
||||
// Replace with content transition
|
||||
let newImage = UIImage(systemName: "pause.fill")!
|
||||
imageView.setSymbolImage(newImage, contentTransition: .replace)
|
||||
|
||||
// Directional replace
|
||||
imageView.setSymbolImage(newImage, contentTransition: .replace.downUp)
|
||||
imageView.setSymbolImage(newImage, contentTransition: .replace.upUp)
|
||||
imageView.setSymbolImage(newImage, contentTransition: .replace.offUp)
|
||||
|
||||
// With options
|
||||
imageView.setSymbolImage(newImage, contentTransition: .replace, options: .speed(2.0))
|
||||
```
|
||||
|
||||
### UIBarButtonItem Effects
|
||||
|
||||
```swift
|
||||
// Effects also work on UIBarButtonItem
|
||||
barButtonItem.addSymbolEffect(.bounce)
|
||||
barButtonItem.addSymbolEffect(.pulse, isActive: isLoading)
|
||||
barButtonItem.removeSymbolEffect(ofType: PulseSymbolEffect.self)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 9: Accessibility
|
||||
|
||||
### Labels
|
||||
|
||||
```swift
|
||||
// SwiftUI
|
||||
Image(systemName: "star.fill")
|
||||
.accessibilityLabel("Favorite")
|
||||
|
||||
// UIKit
|
||||
let image = UIImage(systemName: "star.fill")
|
||||
imageView.accessibilityLabel = "Favorite"
|
||||
imageView.isAccessibilityElement = true
|
||||
|
||||
// Label automatically provides accessibility
|
||||
Label("Settings", systemImage: "gear")
|
||||
// VoiceOver reads: "Settings"
|
||||
```
|
||||
|
||||
### Reduce Motion
|
||||
|
||||
Symbol effects automatically respect `UIAccessibility.isReduceMotionEnabled`. When Reduce Motion is on:
|
||||
- Most effects are simplified or suppressed
|
||||
- Replace transitions use crossfade instead of directional movement
|
||||
- Indefinite effects may be simplified to static appearance changes
|
||||
|
||||
**Do not** attempt to override or check this yourself for effects. The system handles it. Only intervene if effects carry semantic meaning:
|
||||
|
||||
```swift
|
||||
// If the pulsing conveys connection status, provide a text label
|
||||
Image(systemName: "wifi")
|
||||
.symbolEffect(.pulse, isActive: isConnecting)
|
||||
.accessibilityLabel(isConnecting ? "Connecting to WiFi" : "WiFi connected")
|
||||
```
|
||||
|
||||
### Bold Text
|
||||
|
||||
SF Symbols automatically adapt when Bold Text is enabled in Accessibility settings. Custom symbols need weight variants to support this properly.
|
||||
|
||||
### Dynamic Type
|
||||
|
||||
Symbols sized with `.font()` scale automatically with Dynamic Type. Symbols sized with explicit point sizes (`.font(.system(size: 24))`) do **not** scale.
|
||||
|
||||
```swift
|
||||
// ✅ Scales with Dynamic Type
|
||||
Image(systemName: "star.fill")
|
||||
.font(.title)
|
||||
|
||||
// ❌ Fixed size, does not scale
|
||||
Image(systemName: "star.fill")
|
||||
.font(.system(size: 24))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 10: Common Patterns
|
||||
|
||||
### Notification Badge with Effect
|
||||
|
||||
```swift
|
||||
struct NotificationBell: View {
|
||||
let count: Int
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: count > 0 ? "bell.badge.fill" : "bell.fill")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.symbolEffect(.wiggle, value: count)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(count > 0 ? .red : .primary, .primary)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WiFi Strength Indicator
|
||||
|
||||
```swift
|
||||
struct WiFiIndicator: View {
|
||||
let strength: Double // 0.0 to 1.0
|
||||
let isSearching: Bool
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: "wifi", variableValue: strength)
|
||||
.symbolEffect(.variableColor.iterative, isActive: isSearching)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.accessibilityLabel(
|
||||
isSearching ? "Searching for WiFi" :
|
||||
"WiFi strength: \(Int(strength * 100))%"
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Animated Toggle
|
||||
|
||||
```swift
|
||||
struct RecordButton: View {
|
||||
@State private var isRecording = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
isRecording.toggle()
|
||||
} label: {
|
||||
Image(systemName: isRecording ? "stop.circle.fill" : "record.circle")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.symbolEffect(.breathe.pulse, isActive: isRecording)
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(isRecording ? .red : .primary)
|
||||
}
|
||||
.accessibilityLabel(isRecording ? "Stop recording" : "Start recording")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-State Symbol with Draw (iOS 26+)
|
||||
|
||||
```swift
|
||||
struct TaskCheckbox: View {
|
||||
@State private var isComplete = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
isComplete.toggle()
|
||||
} label: {
|
||||
Image(systemName: isComplete ? "checkmark.circle.fill" : "circle")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
.symbolEffect(.drawOn, isActive: isComplete)
|
||||
.font(.title2)
|
||||
.foregroundStyle(isComplete ? .green : .secondary)
|
||||
}
|
||||
.accessibilityLabel(isComplete ? "Completed" : "Not completed")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2023-10257, 2023-10258, 2024-10188, 2025-337
|
||||
|
||||
**Docs**: /symbols, /symbols/symboleffect, /symbols/bouncesymboleffect, /symbols/pulsesymboleffect, /symbols/variablecolorsymboleffect, /symbols/scalesymboleffect, /symbols/wigglesymboleffect, /symbols/rotatesymboleffect, /symbols/breathesymboleffect, /symbols/appearsymboleffect, /symbols/disappearsymboleffect, /symbols/replacesymboleffect, /symbols/drawonsymboleffect, /symbols/drawoffsymboleffect, /swiftui/image/symbolrenderingmode(_:), /uikit/uiimage/symbolconfiguration
|
||||
|
||||
**Skills**: axiom-sf-symbols, axiom-hig-ref, axiom-swiftui-animation-ref
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** Based on WWDC 2023/10257-10258, WWDC 2024/10188, WWDC 2025/337
|
||||
**Version** iOS 13+ (display), iOS 15+ (rendering modes), iOS 17+ (effects), iOS 18+ (Wiggle/Rotate/Breathe), iOS 26+ (Draw, Gradients)
|
||||
Reference in New Issue
Block a user