Use when the user wants to create a custom OUDS theme or brand theme for an iOS app — covers subclassing OrangeTheme, building a theme from scratch on OUDSTheme, mixing existing providers, local custom fonts (.ttf registration), and tuning.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Use when the user wants to create a custom OUDS theme or brand theme for an iOS app — covers subclassing OrangeTheme, building a theme from scratch on OUDSTheme, mixing existing providers, local custom fonts (.ttf registration), and tuning.
license
MIT
Skill: ouds-ios-guide-create-theme
Step-by-step guide to create a custom OUDS theme for an iOS/iPadOS app.
0. Prerequisites — ask the user first
Before writing any code, ask the user these two questions:
Question 1 — Which strategy?
Strategy
When to choose
A — Subclass OrangeTheme(recommended)
Orange brand assets are needed; only some tokens differ from Orange defaults
B — From scratch on OUDSTheme
Fully independent brand (no Orange assets); all tokens are custom
C — Mix existing providers
Compose providers from existing themes without creating a new subclass
Question 2 — Custom fonts?
Does the theme need to embed .ttf font files? If yes, collect:
Font family name (as shown in Font Book)
PostScript name for each weight used
1. Inheritance hierarchy
OUDSTheme ← base "abstract" class (open)
│
└── OrangeTheme ← open, the ONLY publicly subclassable theme
│
└── YourTheme ← your app's custom theme (Strategy A)
OUDSTheme
└── YourTheme ← from-scratch theme (Strategy B)
Rule: Only OrangeTheme may be subclassed by external code.
SoshTheme, OrangeCompactTheme, and WireframeTheme are all final.
2. Strategy A — Subclass OrangeTheme(recommended)
2.1 Import
import OUDSThemesOrange
// or the umbrella product that includes it:import OUDSSwiftUIOrange
2.2 Override only the providers you need
Each provider inherits from an OrangeThemeXxxProvider class. Override @objc open properties.
Semantic token providers available to override:
What to override
Orange base class to inherit
Borders (style / width / radius)
OrangeThemeBorderSemanticTokensProvider
Colors (light + dark via MultipleColorSemanticToken)
import OUDSThemesContract
classYourTheme: OUDSTheme {
staticlet name ="YourBrand"overrideinit() {
// Only instantiate providers you need to override.// All other parameters default to the Orange equivalents.let colors =YourThemeColorProvider()
let borders =YourThemeBorderProvider()
let fonts =YourThemeFontProvider()
// … add only the providers you override …super.init(
colors: colors,
borders: borders,
fonts: fonts,
// Leave unspecified parameters as nil → Orange defaults are used.
name: Self.name,
tuning: Tuning.default, // see §5 for tuning options
hasTypographyHeadingLargeMarker: true// see §5.1, OUDSTheme subclass required for this parameter
)
}
}
All super.init parameters are optional (? = nil). Omit those you don't override.
3. Strategy B — From scratch on OUDSTheme
Warning: This requires implementing all providers — potentially hundreds of @objc open
property overrides. Use only for fully independent brands with no Orange assets.
See SoshTheme in the OUDS source as the canonical reference.
3.1 Import
import OUDSThemesContract // OUDSTheme base + AllXxx protocolsimport SwiftUI
import CoreText // only if registering custom fonts
3.2 Provider instantiation order
Respect dependency order — some providers take others as constructor arguments:
1. borders, colors, effects, elevations, fonts, grids, opacities
2. dimensions ← standalone
3. colorModes(colors:) ← depends on colors
4. sizes(dimensions:), spaces(dimensions:) ← depend on dimensions
5. All component providers ← depend on the semantic providers above
3.3 Full AllXxx protocol list
Semantic providers (all mandatory except ✦):
super.init parameter
Protocol to implement
Notes
borders
AllBorderSemanticTokensProvider
colors
AllColorSemanticTokensProvider
colorModes
AllColorModeSemanticTokensProvider
depends on colors
colorsCharts
AllColorChartSemanticTokensProvider
✦ optional, may be nil
colorsDecorative
AllColorDecorativeSemanticTokensProvider
✦ optional, may be nil
effects
AllEffectSemanticTokensProvider
elevations
AllElevationSemanticTokensProvider
fonts
AllFontSemanticTokensProvider
grids
AllGridSemanticTokensProvider
opacities
AllOpacitySemanticTokensProvider
dimensions
AllDimensionSemanticTokensProvider
sizes
AllSizeSemanticTokensProvider
depends on dimensions
spaces
AllSpaceSemanticTokensProvider
depends on dimensions
Theme flags (Strategy B):
Parameter
Type
Description
hasTypographyHeadingLargeMarker
Bool
If true, displays a decorative marker below OUDSHeading when size == .large and hasMarker: true. Default: false.
Component providers (all mandatory):
super.init parameter
Protocol to implement
alert
AllAlertComponentTokensProvider
badge
AllBadgeComponentTokensProvider
bar
AllBarComponentTokensProvider
bulletList
AllBulletListComponentTokensProvider
button
AllButtonComponentTokensProvider
checkbox
AllCheckboxComponentTokensProvider
chip
AllChipComponentTokensProvider
divider
AllDividerComponentTokensProvider
icon
AllIconComponentTokensProvider
link
AllLinkComponentTokensProvider
listItem
AllListItemComponentTokensProvider
pinCodeInput
AllPinCodeInputComponentTokensProvider
quantityInput
AllQuantityInputComponentTokensProvider
radioButton
AllRadioButtonComponentTokensProvider
selectInput
AllSelectInputComponentTokensProvider
skeleton
AllSkeletonComponentTokensProvider
switch
AllSwitchComponentTokensProvider
tag
AllTagComponentTokensProvider
inputTag
AllInputTagComponentTokensProvider
textArea
AllTextAreaComponentTokensProvider
textInput
AllTextInputComponentTokensProvider
3.4 Theme class skeleton
import Foundation
import OUDSThemesContract
import SwiftUI
// import CoreText ← add only if using custom fonts// swiftlint:disable function_body_lengthpublicfinalclassYourTheme: OUDSTheme, @unchecked Sendable {
publicstaticlet name ="YourBrand"nonisolated(unsafe) privatestaticvar fontsAlreadyRegistered =falsepublicinit() {
// ── Semantic providers ──────────────────────────────────────────let borders =YourThemeBorderSemanticTokensProvider()
let colors =YourThemeColorSemanticTokensProvider()
let colorModes =YourThemeColorModeSemanticTokensProvider(colors: colors)
// let colorsCharts = YourThemeColorChartSemanticTokensProvider() // optional// let colorsDecorative = YourThemeColorDecorativeSemanticTokensProvider() // optionallet effects =YourThemeEffectSemanticTokensProvider()
let elevations =YourThemeElevationSemanticTokensProvider()
let fonts =YourThemeFontSemanticTokensProvider()
let grids =YourThemeGridSemanticTokensProvider()
let opacities =YourThemeOpacitySemanticTokensProvider()
let dimensions =YourThemeDimensionSemanticTokensProvider()
let sizes =YourThemeSizeSemanticTokensProvider(dimensions: dimensions)
let spaces =YourThemeSpaceSemanticTokensProvider(dimensions: dimensions)
// ── Component providers ─────────────────────────────────────────let alert =YourThemeAlertComponentTokensProvider(sizes: sizes, borders: borders, spaces: spaces)
let badge =YourThemeBadgeComponentTokensProvider(spaces: spaces, dimensions: dimensions)
let bar =YourThemeBarComponentTokensProvider(sizes: sizes, borders: borders, colors: colors, opacities: opacities, effects: effects)
let bulletList =YourThemeBulletListComponentTokensProvider(spaces: spaces)
let button =YourThemeButtonComponentTokensProvider(sizes: sizes, borders: borders, colors: colors, spaces: spaces)
let checkbox =YourThemeCheckboxComponentTokensProvider(sizes: sizes, borders: borders)
let chip =YourThemeChipComponentTokensProvider(sizes: sizes, borders: borders, colors: colors, spaces: spaces, dimensions: dimensions)
let divider =YourThemeDividerComponentTokensProvider(borders: borders)
let icon =YourThemeIconComponentTokensProvider(colors: colors)
let link =YourThemeLinkComponentTokensProvider(sizes: sizes, colors: colors, spaces: spaces)
let listItem =YourThemeListItemComponentTokensProvider(sizes: sizes, borders: borders, colors: colors, spaces: spaces, dimensions: dimensions)
let pinCodeInput =YourThemePinCodeInputComponentTokensProvider(spaces: spaces, dimensions: dimensions)
let quantityInput =YourThemeQuantityInputComponentTokensProvider(sizes: sizes, spaces: spaces)
let radioButton =YourThemeRadioButtonComponentTokensProvider(sizes: sizes, borders: borders)
let selectInput =YourThemeSelectInputComponentTokensProvider(dimensions: dimensions)
let skeleton =YourThemeSkeletonComponentTokensProvider(colors: colors)
let `switch` =YourThemeSwitchComponentTokensProvider(sizes: sizes, borders: borders, colors: colors, spaces: spaces, opacities: opacities, dimensions: dimensions)
let tag =YourThemeTagComponentTokensProvider(sizes: sizes, borders: borders, spaces: spaces, dimensions: dimensions)
let inputTag =YourThemeInputTagComponentTokensProvider(borders: borders, colors: colors)
let textArea =YourThemeTextAreaComponentTokensProvider(sizes: sizes, spaces: spaces)
let textInput =YourThemeTextInputComponentTokensProvider(sizes: sizes, borders: borders, colors: colors, spaces: spaces, dimensions: dimensions)
// ── Super init ──────────────────────────────────────────────────super.init(
borders: borders,
colors: colors,
colorModes: colorModes,
// colorsCharts: colorsCharts, // omit if not used// colorsDecorative: colorsDecorative, // omit if not used
effects: effects,
elevations: elevations,
fonts: fonts,
grids: grids,
opacities: opacities,
dimensions: dimensions,
sizes: sizes,
spaces: spaces,
alert: alert,
badge: badge,
bar: bar,
bulletList: bulletList,
button: button,
checkbox: checkbox,
chip: chip,
divider: divider,
icon: icon,
link: link,
listItem: listItem,
pinCodeInput: pinCodeInput,
quantityInput: quantityInput,
radioButton: radioButton,
selectInput: selectInput,
skeleton: skeleton,
switch: `switch`,
tag: tag,
inputTag: inputTag,
textArea: textArea,
textInput: textInput,
resourcesBundle: Bundle.YourTheme, // see §6 for custom fonts
name: Self.name,
fontFamily: "YourFontFamilyName", // nil = system font
tuning: Tuning.default,
hasTypographyHeadingLargeMarker: true) // see §5.1
registerFonts() // only if using custom fonts — see §6
}
deinit {}
}
// swiftlint:enable function_body_length
4. Strategy C — Mix existing providers
No new subclass needed. Instantiate providers from existing themes and pass them directly to OrangeTheme:
import OUDSThemesOrange
// Reuse most Orange providers, only replace colors:let dimensions =OrangeThemeDimensionSemanticTokensProvider()
let borders =OrangeThemeBorderSemanticTokensProvider()
let colors =YourOwnColorSemanticTokensProvider() // customlet sizes =OrangeThemeSizeSemanticTokensProvider(dimensions: dimensions)
let spaces =OrangeThemeSpaceSemanticTokensProvider(dimensions: dimensions)
// Component providers that depend on colors must receive the custom one:let button =OrangeThemeButtonComponentTokensProvider(
sizes: sizes, borders: borders, colors: colors, spaces: spaces)
// Inject directly — no subclass required:let theme =OrangeTheme(colors: colors, button: button)
Or wrap in a named class for reuse across the app:
classYourTheme: OrangeTheme {
overrideinit() {
let dimensions =OrangeThemeDimensionSemanticTokensProvider()
let borders =OrangeThemeBorderSemanticTokensProvider()
let colors =YourOwnColorSemanticTokensProvider()
let sizes =OrangeThemeSizeSemanticTokensProvider(dimensions: dimensions)
let spaces =OrangeThemeSpaceSemanticTokensProvider(dimensions: dimensions)
let button =OrangeThemeButtonComponentTokensProvider(
sizes: sizes, borders: borders, colors: colors, spaces: spaces)
super.init(colors: colors, button: button)
}
}
5. Tuning & Flags
Tuning controls brand-level UI decisions for corner rounding. Only OrangeTheme (and its subclasses) support tuning.
// Custom tuning:let tuning =Tuning(
hasRoundedButtons: true, // rounded corners on buttons
hasRoundedTextInputs: true, // rounded corners on text / PIN / password / text area inputs
hasRoundedAlertMessages: false) // rounded corners on alert messageslet theme =OrangeTheme(tuning: tuning)
// or: YourTheme(tuning: tuning) if your init forwards the parameter// Predefined tunings:OrangeTheme(tuning: Tuning.default) // all falseOrangeTheme(tuning: Tuning.OrangeFrance) // same as defaultOrangeTheme(tuning: Tuning.OrangeBusiness) // rounded inputs + alertsOrangeTheme(tuning: Tuning.MaxIt) // everything rounded
5.1 Theme flags
Additional boolean flags control specific UI behaviors:
Flag
Description
hasTypographyHeadingLargeMarker
If true, displays a decorative marker below OUDSHeading when size == .large and hasMarker: true. Force to true for Orange-style brand markers and Wireframe brand. Subclass OUDSTheme to have the parameter in init (default: false).
// Enable heading marker (e.g., for Orange-style themes):let theme =OUDSTheme(hasTypographyHeadingLargeMarker: true)
// Disable it (default behavior):let theme =OUDSTheme(hasTypographyHeadingLargeMarker: false)
For a from-scratch theme (Strategy B), declare a custom predefined tuning in an extension:
// YourTheme+Bundle.swiftimport Foundation
extensionBundle {
/// The bundle for YourTheme resources (fonts, images, …)publicstaticletYourTheme=Bundle.module
}
6.3 Register fonts at theme init (CoreText)
Call once in init(), guarded by a static flag to prevent duplicate registration errors:
super.init(
// …
fontFamily: "YourFontFamily", // PostScript name or family name — check Font Book on-device// …
)
Pass nil to fall back to the system font.
6.5 Register PostScript names per weight (multi-weight fonts)
When a font family has distinct PostScript names per weight, register the mapping so OUDS can resolve Font objects correctly. Use registerFont(postScript:forCombination:) with the PSFNMK typealias:
kApplePostScriptFontNames is the read-only map used internally by OUDS to resolve weights.
Unregistered weight/family combinations fall back to the family name without weight hints.
6.6 Declare raw tokens for the font family (recommended)