iOS Token Structure: openedx-app-ios
Source: Theme/Theme/Theme.swift, Theme/Theme/SwiftGen/ThemeAssets.swift, Theme/Theme/Assets.xcassets/, Theme/Theme/Fonts/fonts.jsonConfidence: CONFIRMED — verified directly against codebase
Summary
The app uses a Theme module as the single source of truth for all design tokens. Token definitions exist at a semantic / component level only — there is no primitive layer (no gray-100, blue-500, no spacing scale). Colors are stored in Xcode Asset Catalogs with light/dark mode pairs. Typography and shapes are defined directly in Swift. There is no spacing token system; all padding and layout values are hardcoded at the call site.
Token Architecture Overview
Theme/
├── Assets.xcassets/ # Color values (sRGB, light+dark pairs per token)
│ └── Colors/
│ ├── TextColor/ # textPrimary, textSecondary, etc.
│ ├── TextInput/ # textInputBackground, textInputStroke, etc.
│ ├── CardView/ # cardViewBackground, cardViewStroke
│ ├── CourseDates/ # timeline colors, dates section
│ ├── PrimaryCard/ # primaryCardCautionBG, etc.
│ ├── ProgressLine/ # onProgress, progressDone, progressSkip, selectedAndDone
│ ├── ResumeButton/ # resumeButtonBG, resumeButtonText
│ ├── SecondaryButton/ # border, text, BG
│ ├── SlidingTabBar/ # slidingTextColor, etc.
│ ├── Snackbar/ # error, warning, info, text
│ ├── Tabbar/ # active, inactive, BG
│ └── [root-level] # accentColor, background, alert, warning, white, etc.
│
├── Fonts/
│ └── fonts.json # Font family name map (weight → PostScript name)
│
└── Theme.swift # Public API: Theme.Colors, Theme.Fonts, Theme.Shapes
SwiftGen/ThemeAssets.swift # Auto-generated asset enum (SwiftGen)Access pattern in app code:
Theme.Colors.textPrimary // → SwiftUI Color
Theme.UIColors.textPrimary // → UIColor (bridge for UIKit contexts)
Theme.Fonts.bodyLarge // → Font
Theme.Shapes.buttonShape // → ShapeToken Levels: What Exists vs. What's Missing
| Token Level | Colors | Typography | Spacing | Shapes |
|---|---|---|---|---|
| Primitive (raw values, unnamed) | ✗ Not present | ✗ Not present | ✗ Not present | ✗ Not present |
| Semantic (named by role/intent) | ✓ Present | ✓ Present | ✗ Not present | Partial |
| Component (scoped to a component) | Partial (folder grouping) | ✗ Not present | ✗ Not present | Partial |
Bottom line: The app skips primitive tokens entirely. Every token is named by semantic intent or component context. There is no underlying palette that semantic tokens reference — the raw hex values live directly in the Asset Catalog.
Colors
How They're Defined
Color values are stored in Xcode Asset Catalog .colorset files as sRGB float components. Each colorset contains a light mode and optionally a dark mode variant.
Example — TextPrimary.colorset/Contents.json:
{
"colors": [
{
"color": { "color-space": "srgb", "components": { "red": "0.098", "green": "0.129", "blue": "0.184", "alpha": "1.000" } },
"idiom": "universal"
},
{
"appearances": [{ "appearance": "luminosity", "value": "dark" }],
"color": { "color-space": "srgb", "components": { "red": "1.000", "green": "1.000", "blue": "1.000", "alpha": "1.000" } },
"idiom": "universal"
}
]
}Colors are auto-enumerated by SwiftGen into ThemeAssets, then re-exposed as Theme.Colors static properties with nonisolated(unsafe) modifiers (to support white-label runtime overrides).
Token Inventory (91 total)
General / Surface
| Token | Notes |
|---|---|
background | App background |
loginBackground | Login screen background |
backgroundStroke | Border on background surfaces |
cardViewBackground | Card surface |
cardViewStroke | Card border |
commentCellBackground | Discussion comment cell |
courseCardBackground | Course card surface |
courseCardShadow | Course card drop shadow |
datesSectionBackground | Dates tab section |
datesSectionStroke | Dates tab section border |
deleteAccountBG | Delete account screen |
loginNavigationText | Login nav bar text |
shadowColor | Generic shadow |
shade | Overlay/scrim |
white | Pure white (used as explicit override) |
Accent / Brand
| Token | Notes |
|---|---|
accentColor | Primary brand color |
accentXColor | Alternate accent |
accentButtonColor | Button variant of accent |
Text
| Token | Notes |
|---|---|
textPrimary | Primary body text |
textSecondary | Secondary/muted text |
textSecondaryLight | Light variant |
textSecondaryDark | Dark variant |
textInputBackground | Focused input BG |
textInputStroke | Focused input border |
textInputUnfocusedBackground | Unfocused input BG |
textInputUnfocusedStroke | Unfocused input border |
textInputTextColor | Input text |
textInputPlaceholderColor | Placeholder text |
Buttons
| Token | Notes |
|---|---|
styledButtonText | Primary button text |
disabledButton | Disabled button BG |
disabledButtonText | Disabled button text |
primaryButtonTextColor | Primary button text (alt) |
secondaryButtonBGColor | Secondary button BG |
secondaryButtonBorderColor | Secondary button border |
secondaryButtonTextColor | Secondary button text |
resumeButtonBG | Resume course button BG |
resumeButtonText | Resume course button text |
socialAuthColor | Social login button |
toggleSwitchColor | Toggle/switch |
Semantic Status
| Token | Notes |
|---|---|
alert | Error/alert |
irreversibleAlert | Destructive action alert |
warning | Warning state |
warningText | Warning text |
success | Success state |
infoColor | Informational |
Progress / Course
| Token | Notes |
|---|---|
onProgress | In-progress state |
progressDone | Completed state |
progressSkip | Skipped state |
progressSelectedAndDone | Selected + completed |
courseProgressBG | Course progress bar BG |
circleProgressBG | Circle progress BG |
progressLineBG | Progress line BG |
progressPercentage | Progress percentage text |
assignmentColor | Assignment item stroke |
Snackbar
| Token | Notes |
|---|---|
snackbarErrorColor | Error snackbar |
snackbarWarningColor | Warning snackbar |
snackbarInfoColor | Info snackbar |
snackbarTextColor | Snackbar text |
Timeline (Course Dates)
| Token | Notes |
|---|---|
todayTimelineColor | Today marker |
thisWeekTimelineColor | This week |
nextWeekTimelineColor | Next week |
upcomingTimelineColor | Upcoming |
pastDueTimelineColor | Past due |
Course Headers
| Token | Notes |
|---|---|
primaryHeaderColor | Primary course header |
secondaryHeaderColor | Secondary course header |
Primary Card States
| Token | Notes |
|---|---|
primaryCardCautionBG | Caution state card |
primaryCardUpgradeBG | Upgrade CTA card |
primaryCardProgressBG | Progress state card |
Navigation / Tabbar
| Token | Notes |
|---|---|
navigationBarTintColor | Nav bar icons/text |
tabbarActiveColor | Active tab icon |
tabbarInactiveColor | Inactive tab icon |
tabbarBGColor | Tab bar background |
Sliding Tab Bar
| Token | Notes |
|---|---|
slidingTextColor | Unselected tab text |
slidingSelectedTextColor | Selected tab text |
slidingStrokeColor | Tab bar border |
Other
| Token | Notes |
|---|---|
avatarStroke | Avatar border |
certificateForeground | Certificate illustration |
loginNavigationText | Auth flow nav text |
White-Label Override
Theme.Colors.update(...) allows runtime replacement of any color, enabling multi-tenant theming without recompilation. The update() function signature is partial — not all 91 tokens are overridable via it (only ~35 are exposed), leaving the remainder as asset-only values.
Typography
How It's Defined
Font tokens are Swift static constants in Theme.Fonts. Font weight is abstracted through a FontIdentifier enum mapped to PostScript names via fonts.json. This allows white-label font replacement: swap fonts.json to change the entire typeface.
Font weight map (fonts.json):
{
"light": "SFPro-Light",
"regular": "SFPro-Regular",
"medium": "SFPro-Medium",
"semiBold": "SFPro-Semibold",
"bold": "SFPro-Bold"
}Type Scale
The scale follows Material Design role vocabulary (display, headline, title, body, label) × size (large, medium, small). Sizes are hardcoded integers — there is no underlying spacing or size scale they reference.
| Token | Weight | Size |
|---|---|---|
displayLarge | Regular | 57pt |
displayMedium | Regular | 45pt |
displaySmall | Bold | 36pt |
headlineLarge | Regular | 32pt |
headlineMedium | Regular | 28pt |
headlineSmall | Regular | 24pt |
titleLarge | Bold | 22pt |
titleMedium | SemiBold | 18pt |
titleSmall | Medium | 14pt |
bodyLarge | Regular | 16pt |
bodyMedium | Regular | 14pt |
bodySmall | Regular | 12pt |
bodyMicro | Light | 11pt |
labelLarge | Medium | 14pt |
labelMedium | Regular | 12pt |
labelSmall | Regular | 10pt |
UIKit bridge (Theme.UIFonts): Three UIFont variants exposed for contexts where SwiftUI Font cannot be used: labelSmall, labelLarge, titleMedium.
Shapes / Radius
Defined as Theme.Shapes static properties. Mix of configurable and hardcoded values.
| Token | Value | Notes |
|---|---|---|
screenBackgroundRadius | 24.0pt | Fixed |
cardImageRadius | 10.0pt | Fixed |
buttonCornersRadius | 8.0pt | Configurable via isRoundedCorners flag |
cardShape | 12pt all corners | Fixed |
unitButtonShape | 21pt all corners | Fixed (pill-like) |
textInputShape | 8pt (or 0 if not rounded) | Responds to isRoundedCorners |
buttonShape | buttonCornersRadius (or 0) | Responds to isRoundedCorners |
roundedScreenBackgroundShape | 24pt all corners | Full rounded screen |
roundedScreenBackgroundShapeCroppedBottom | 24pt top only | Sheet-style partial rounding |
isRoundedCorners is a global toggle (default: true) that flattens button and input corners for operators who prefer a rectangular aesthetic.
Spacing
There is no spacing token system. No Theme.Spacing struct exists. All padding, gap, and margin values are hardcoded inline at each call site across the codebase. The only spacing-adjacent value in Theme is the default padding: CGFloat = 8 on InputFieldBackground, which is a component default, not a token.
This is the most significant gap relative to a mature token architecture.
Key Design Observations
What the token system does well
- Semantic naming throughout — no
blue1orcolor23; every token has a clear intent - Dark mode built in — every
.colorsetis a light/dark pair, handled at the asset level - White-label ready —
Colors.update()andfonts.jsonsubstitution enable multi-tenant theming - SwiftGen keeps the asset enum in sync automatically, preventing stale references
- Concurrency-safe —
nonisolated(unsafe)on mutable statics is appropriate for Swift 6 concurrency
What's missing vs. a token hierarchy
- No primitive layer — raw color values (hex/sRGB) live directly in Asset Catalog files with no named palette. There's no
brand-bluethataccentColorreferences. - No spacing scale — no
4 / 8 / 12 / 16 / 24 / 32scale; all spacing is magic numbers in views - No elevation tokens — shadow values are not tokenized;
shadowColoris a color only, no blur/offset definitions - Inconsistent component scoping — some colors are folder-grouped by component (
SecondaryButton/,Snackbar/) but the grouping is cosmetic only; it's not enforced in code - Partial
update()coverage — 91 colors are defined but only ~35 are in theupdate()override signature; late-added tokens are missing from the white-label path - No line-height or letter-spacing tokens — typography tokens define only size and weight; line-height and tracking are left to SwiftUI defaults or hardcoded per view
Files Referenced
| File | Purpose |
|---|---|
Theme/Theme/Theme.swift | Public token API — Theme.Colors, Theme.Fonts, Theme.Shapes |
Theme/Theme/SwiftGen/ThemeAssets.swift | Auto-generated SwiftGen enum for all color and image assets |
Theme/Theme/Assets.xcassets/Colors/** | Raw color values (sRGB, light/dark pairs) |
Theme/Theme/Fonts/FontParser.swift | Font weight → PostScript name resolution |
Theme/Theme/Fonts/fonts.json | Font family name map (white-label font config) |
Theme/Theme/Helpers/RoundedCorners.swift | Custom Shape for per-corner radius control |