| name | ouds-ios-framework-usage |
| description | How to set up and use the OUDS iOS framework with imports, themes, tokens, view modifiers, and all available components with code examples |
| license | MIT |
OUDS Framework Usage
1. Basic setup
import OUDSSwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
OUDSThemeableView(theme: OrangeTheme()) {
ContentView()
}
}
}
}
struct ContentView: View {
@Environment(\.theme) private var theme
var body: some View {
VStack(spacing: theme.spaces.fixedMedium) {
OUDSButton(text: "Action", appearance: .default) {}
}
.padding(theme.spaces.fixedLarge)
}
}
2. Imports — two modes
OUDS exposes two layers of Swift Package products. Choose one approach per project; never mix them.
Mode 1 — Umbrella import (recommended)
One import pulls every required dependency. Prefer this unless you have a strong reason to minimize compiled targets.
| Product | Themes included | When to use |
|---|
OUDSSwiftUI | All (Orange, OrangeCompact, Sosh, Wireframe) | Default — use this when in doubt |
OUDSSwiftUIOrange | Orange + OrangeCompact | Orange-brand apps only |
OUDSSwiftUIOrangeSosh | Orange + Sosh | Multi-brand Orange/Sosh apps |
OUDSSwiftUIWireframe | Wireframe only | Prototyping, no brand theme needed |
import OUDSSwiftUI
import OUDSSwiftUIOrange
import OUDSSwiftUIOrangeSosh
import OUDSSwiftUIWireframe
Mode 2 — Granular (atomic) import
For advanced users who want fine-grained control over compiled dependencies. Import only what the target actually needs.
| Product | Content |
|---|
OUDSThemesOrange | Orange theme |
OUDSThemesOrangeCompact | Orange Compact theme (depends on OrangeTheme) |
OUDSThemesSosh | Sosh theme |
OUDSThemesWireframe | Wireframe theme (depends on OrangeTheme) |
OUDSThemesContract | Theme protocols / contracts — required when using any theme |
OUDSModules | High-level screen-ready modules (depends on OUDSComponents) |
OUDSComponents | SwiftUI components |
OUDSTokensComponent | Component-level design tokens |
OUDSTokensRaw | Raw tokens + semantic tokens |
OUDSFoundations | Base utilities (lowest level) |
import OUDSComponents
import OUDSThemesContract
import OUDSThemesOrange
Rule: always prefer an umbrella product. Use granular imports only when you explicitly need to exclude specific themes or layers from compilation.
3. Themes
OUDSThemeableView(theme: OrangeTheme()) { … }
@Environment(\.theme) private var theme
Available: OrangeTheme, OrangeCompact, SoshTheme, WireframeTheme.
4. Token namespaces
| Namespace | Content |
|---|
theme.colors | Color semantic tokens |
theme.colorModes | Color-mode tokens (for OUDSColoredSurface) |
theme.borders | Border style / width / radius |
theme.fonts | Typography |
theme.sizes | Sizes |
theme.spaces | Spacing |
theme.dimensions | Dimensions |
theme.elevations | Shadows / elevation |
theme.grids | Grid |
theme.opacities | Opacity |
theme.effects | Visual effects |
5. View modifiers
Text("Hello")
.font(theme.fonts.bodyDefaultMedium)
.foregroundColor(theme.colors.contentDefault)
.background(theme.colors.bgPrimary)
.border(style: theme.borders.styleDefault,
width: theme.borders.widthThin,
radius: theme.borders.radiusMedium,
color: theme.colors.actionEnabled)
.shadow(theme.elevations.emphasized)
For Shape types, use fill(_:style:) with a color token — the light/dark variant is resolved automatically:
Circle()
.fill(theme.colors.actionEnabled)
RoundedRectangle(cornerRadius: 8)
.fill(theme.colors.bgPrimary, style: FillStyle(eoFill: true))
5b. Images in OUDS components
Never call SwiftUI modifiers on an Image that is passed as a parameter to an OUDS component.
OUDS components accept Image — the bare SwiftUI type. Calling any modifier on it (including .accessibilityHidden(true)) changes the type to some View and produces a compile error.
The component handles the accessibility of its own images internally. You must not alter them from the call site.
Never do this:
OUDSButton(
text: "Add",
image: Image(systemName: "plus").accessibilityHidden(true),
appearance: .default) {}
OUDSLink(
text: "Back",
image: OUDSImage(asset: Image(systemName: "chevron.left").accessibilityHidden(true)),
size: .default) {}
Always do this — pass a bare Image inside OUDSImage, swiftlint comment on the line before:
OUDSLink(text: "Back", image: OUDSImage(asset: Image(systemName: "chevron.left")), size: .default) {}
OUDSToolBarItem(icon: Image("ic_share"), accessibilityLabel: "Share") {}
The // swiftlint:disable:next accessibility_label_for_image comment must appear on the line immediately before the component call, never on the line of the Image(...) itself.
Exception: Image(decorative: "name") suppresses the linter rule automatically and needs no comment.
6. Common patterns (shared by multiple components)
These patterns apply to Checkbox, Radio, Switch, TextInput, TextArea, PinCodeInput, PasswordInput unless noted otherwise.
| Pattern | Syntax |
|---|
| Error state | isError: true, errorText: "Message" |
| Rich error | errorText: AttributedString |
| Plain helper | helperText: "…" |
| Rich helper | helperText: AttributedString |
| Error status (inputs) | status: .error(message: "…") |
| Rich error status | status: .richError(message: AttributedString) |
| Read-only | isReadOnly: true |
| Disabled | .disabled(true) — never combine with isError or isReadOnly |
Forbidden combinations (fatal error at runtime): isError + isReadOnly, isError + .disabled, isReadOnly + .disabled
7. Components
Index: Button · BulletList · Checkbox · Radio · Switch · PinCode · Password · Chips · TextInput · TextArea · AlertMessage · InlineAlert · Badge · Tag · InputTag · ColoredSurface · Divider · Link · TabBar · Toolbars
Actions — Button
OUDSButton(text: "Label", appearance: .default) {}
OUDSButton(text: "Label", appearance: .default, style: .loading) {}
OUDSButton(text: "Label", image: OUDSImage(asset: Image("ic")), appearance: .default) {}
OUDSButton(image: OUDSImage(asset: Image("ic")), accessibilityLabel: "Label") {}
Content Display — Bullet List
OUDSBulletList {
OUDSBulletList.Item("Label 1")
OUDSBulletList.Item("Label 2")
}
OUDSBulletList(type: .ordered) {
OUDSBulletList.Item("Label 1") { OUDSBulletList.Item("Label 1.1") }
}
OUDSBulletList { OUDSBulletList.Item(AttributedString(…)) }
Controls — Checkbox
OUDSCheckbox(isOn: $isOn, accessibilityLabel: "Label")
OUDSCheckboxIndeterminate(selection: $selection, accessibilityLabel: "Label")
OUDSCheckboxItem("Label", isOn: $isOn)
OUDSCheckboxItem("Label", isOn: $isOn, description: "Helper",
image: OUDSImage(asset: Image(decorative: "ic")))
OUDSCheckboxItem("Label", isOn: $isOn,
image: OUDSImage(asset: Image(decorative: "ic")), isReversed: true)
OUDSCheckboxItem("Label", isOn: $isOn,
image: OUDSImage(asset: Image(decorative: "il_brand"), renderingMode: .original))
OUDSCheckboxItem("Label", isOn: $isOn,
image: OUDSImage(asset: Image(systemName: "figure.handball"),
flipped: layoutDirection == .rightToLeft))
OUDSCheckboxItem(LocalizedStringKey("agree_terms"), bundle: Bundle.module, isOn: $isOn,
image: OUDSImage(asset: Image(decorative: "ic")))
OUDSCheckboxItemIndeterminate("Label", selection: $selection,
image: OUDSImage(asset: Image(decorative: "ic")))
OUDSCheckboxItemIndeterminate(LocalizedStringKey("select_all"), bundle: Bundle.module,
selection: $selection)
Parameter order: (_ label:, isOn:, description:, image:, isReversed:, isError:, errorText:, isReadOnly:, hasDivider:, constrainedMaxWidth:, action:)
OUDSCheckboxPicker(selections: $selections, checkboxes: [
.init(tag: "a", label: "Option A"),
.init(tag: "b", label: "Option B", description: "Details", isReversed: true),
.init(tag: "c", label: "Option C", image: OUDSImage(asset: Image(systemName: "flame"))),
.init(tag: "d", label: "Option D",
image: OUDSImage(asset: Image(decorative: "il_brand"), renderingMode: .original)),
])
OUDSCheckboxPicker(selections: $selections, checkboxes: data,
placement: .verticalRooted("All options", .textAndCount))
OUDSCheckboxPicker(selections: $selections, checkboxes: data,
isReversed: true, placement: .horizontal(true))
Controls — Radio Button
OUDSRadio(isOn: $isOn, accessibilityLabel: "Label")
OUDSRadioItem("Label", isOn: $isOn)
OUDSRadioItem("Label", isOn: $isOn, image: OUDSImage(asset: Image(decorative: "ic")))
OUDSRadioItem("Label", isOn: $isOn,
image: OUDSImage(asset: Image(decorative: "il_brand"), renderingMode: .original))
OUDSRadioItem("Label", isOn: $isOn,
image: OUDSImage(asset: Image(systemName: "chevron.right"),
flipped: layoutDirection == .rightToLeft))
OUDSRadioItem(LocalizedStringKey("option_label"), bundle: Bundle.module, isOn: $isOn,
image: OUDSImage(asset: Image(decorative: "ic")))
OUDSRadioPicker(selection: $selection,
radios: [
.init(tag: "a", label: "Option A"),
.init(tag: "b", label: "Option B",
image: OUDSImage(asset: Image(systemName: "flame"))),
.init(tag: "c", label: "Option C",
image: OUDSImage(asset: Image(decorative: "il_brand"),
renderingMode: .original)),
],
placement: .vertical)
Controls — Switch
OUDSSwitch(isOn: $isOn, accessibilityLabel: "Label")
OUDSSwitchItem("Label", isOn: $isOn)
OUDSSwitchItem("Label", isOn: $isOn,
image: OUDSImage(asset: Image(decorative: "ic")))
OUDSSwitchItem("Label", isOn: $isOn,
image: OUDSImage(asset: Image(decorative: "il_brand"), renderingMode: .original))
OUDSSwitchItem("Label", isOn: $isOn,
image: OUDSImage(asset: Image(systemName: "figure.handball"),
flipped: layoutDirection == .rightToLeft))
OUDSSwitchItem(LocalizedStringKey("wifi_setting"), bundle: Bundle.module, isOn: $isOn,
image: OUDSImage(asset: Image(decorative: "ic")))
Controls — Pin Code Input
Available lengths: .four, .six (default).
OUDSPinCodeInput($value)
OUDSPinCodeInput($value, length: .four, autofocus: true)
OUDSPinCodeInput($value, helperText: "Enter your PIN")
Controls — Password Input
status is of type OUDSTextInput.Status (shared with OUDSTextInput).
OUDSPasswordInput(label: "Password", password: $password, isHiddenPassword: $isHidden)
OUDSPasswordInput(label: "Password", password: $password, isHiddenPassword: $isHidden,
placeholder: "Min. 8 chars", prefix: "🔑", lockIcon: true)
Controls — Chips
OUDSSuggestionChip(text: "Label") {}
OUDSSuggestionChip(image: OUDSImage(asset: Image("ic")), text: "Label") {}
OUDSSuggestionChip(image: OUDSImage(asset: Image("ic"), renderingMode: .original), text: "Label") {}
OUDSSuggestionChip(image: OUDSImage(asset: Image("ic")), accessibilityLabel: "Label") {}
OUDSSuggestionChip(image: OUDSImage(asset: Image("ic"), renderingMode: .original), accessibilityLabel: "Label") {}
OUDSFilterChip(text: "Label") {}
OUDSFilterChip(image: OUDSImage(asset: Image("ic")), text: "Label") {}
OUDSFilterChip(image: OUDSImage(asset: Image("ic"), renderingMode: .original), text: "Label") {}
OUDSFilterChip(image: OUDSImage(asset: Image("ic")), accessibilityLabel: "Label") {}
OUDSFilterChip(image: OUDSImage(asset: Image("ic"), renderingMode: .original), accessibilityLabel: "Label") {}
OUDSChipPicker(title: "Title", selection: $selection, chips: [
.init(tag: .value1, layout: .textAndIcon("Label", image: OUDSImage(asset: Image("ic")))),
.init(tag: .value2, layout: .textAndIcon("Brand", image: OUDSImage(asset: Image("ic_brand"), renderingMode: .original))),
.init(tag: .value3, layout: .icon(OUDSImage(asset: Image("ic")), accessibilityLabel: "Label")),
.init(tag: .value4, layout: .icon(OUDSImage(asset: Image("ic_brand"), renderingMode: .original), accessibilityLabel: "Brand")),
])
Controls — Text Input
OUDSTextInput(label: "Label", text: $text)
OUDSTextInput(label: "Label", text: $text, placeholder: "…", prefix: "Pre", suffix: "Suf")
OUDSTextInput(label: "Label", text: $text, leadingImage: OUDSImage(asset: Image("ic")))
OUDSTextInput(label: "Label", text: $text,
leadingImage: OUDSImage(asset: Image("ic"), renderingMode: .original))
OUDSTextInput(label: "Label", text: $text,
leadingImage: OUDSImage(asset: Image("ic"), flipped: layoutDirection == .rightToLeft))
OUDSTextInput(label: "Label", text: $text,
trailingAction: .init(image: OUDSImage(asset: Image("ic")), actionHint: "Hint") {})
OUDSTextInput(label: "Label", text: $text,
trailingAction: .init(image: OUDSImage(asset: Image("ic"), renderingMode: .original),
actionHint: "Hint") {})
Controls — Text Area
helperText type: .plain(String) | .rich(AttributedString) | .charactersMaxCount(UInt16)
OUDSTextArea(label: "Label", text: $text)
OUDSTextArea(label: "Label", text: $text, placeholder: "Describe…")
OUDSTextArea(label: "Label", text: $text, helperText: .plain("Max 500 chars."))
OUDSTextArea(label: "Label", text: $text, helperText: .charactersMaxCount(500))
OUDSTextArea(label: "Label", text: $text,
helperLink: .init(text: "Learn more") { openUrl(url) })
OUDSTextArea(label: "Label", text: $text, constrainedMaxHeight: true)
Height is controlled by two component tokens on theme.textArea:
sizeMinHeightInput (72 pt by default) — minimum height, always applied
sizeMaxHeightInput (240 pt by default) — maximum height before scroll (used when constrainedMaxHeight: false, the default)
When constrainedMaxHeight: true, maxHeight is capped to sizeMinHeightInput, keeping the component at a fixed compact size.
Dialogs — Alert Message
Statuses: neutral, accent, positive, info, warning, negative
OUDSAlertMessage(label: "Label")
OUDSAlertMessage(label: "Label", status: .warning, description: "Details") { }
OUDSAlertMessage(label: "Label",
status: .neutral(image: OUDSImage(asset: Image("ic_heart"), renderingMode: .original)),
bulletList: ["A", "B"],
link: .init(text: "More", position: .bottom) {},
onClose: {})
Dialogs — Inline Alert
Statuses: neutral, accent, positive, info, warning, negative
OUDSInlineAlert(label: "Label")
OUDSInlineAlert(label: "Label", status: .warning)
OUDSInlineAlert(label: "Label", status: .accent(image: OUDSImage(asset: Image("ic_heart"))))
Indicators — Badge
Statuses: neutral, accent, positive, info, warning, negative — Sizes: extraSmall, small, medium, large
Count parameter must be of type UInt8.
OUDSBadgeStandard(accessibilityLabel: "Some label", status: .neutral, size: .medium)
OUDSBadgeCount(3, accessibilityLabel: "Some label", status: .neutral, size: .medium)
OUDSBadgeIcon(status: .neutral(image: OUDSImage(asset: Image("ic"), accessibilityLabel: "Label"), size: .medium))
Indicators — Tag
OUDSTag(label: "Label")
OUDSTag(label: "Label", status: .neutral(image: OUDSImage(asset: Image("ic"))))
OUDSTag(label: "Label", status: .neutral(image: OUDSImage(asset: Image("ic"), renderingMode: .original)))
OUDSTag(label: "Label", status: .neutral(image: OUDSImage(asset: Image("ic"), flipped: true)))
OUDSTag(label: "Label", status: .accent(image: OUDSImage(asset: Image("ic"))))
OUDSTag(label: "Label", status: .accent(image: OUDSImage(asset: Image("ic"), renderingMode: .original)))
OUDSTag(label: "Label", status: .neutral(bullet: true))
Indicators — Input Tag
OUDSInputTag("Label") { }
Layouts — Colored Surface
OUDSColoredSurface(color: theme.colorModes.onStatusPositiveEmphasized) {
}
Layouts — Divider
OUDSHorizontalDivider(color: .brandPrimary)
OUDSVerticalDivider(color: .brandPrimary)
Navigations — Link
OUDSLink(text: "Text", size: .default) {}
OUDSLink(text: "Text", indicator: .back, size: .default) {}
OUDSLink(text: "Text", image: OUDSImage(asset: Image("ic")), size: .default) {}
OUDSLink(text: "Text", image: OUDSImage(asset: Image("ic"), renderingMode: .original), size: .default) {}
Navigations — Tab Bar
Never combine with OUDSToolBarBottom on the same screen.
@State private var selectedTab = 0
OUDSTabBar(selectedTab: $selectedTab, count: 3) {
SomeView().tabItem { Label("Tab 1", image: "ic_1") }.tag(0)
OtherView().tabItem { Label("Tab 2", image: "ic_2") }.tag(1)
}
OUDSTabBar {
SomeView().tabItem { Label("Tab 1", image: "ic_1") }
OtherView().tabItem { Label("Tab 2", image: "ic_2") }
}
Tab bar images: 26×26 pt. OUDSTabBar(selected:count:content:) (plain Int) is deprecated — use selectedTab: Binding<Int>.
Navigations — Toolbars
Availability: iOS 15+, visionOS 1+. Not available on watchOS, tvOS, macOS.
Setup (top toolbar):
- Must be inside
NavigationStack.
- Call
.oudsNavigationBarAppearance() once on the root NavigationStack.
- On iOS ≤ 18: add
.accentColor(theme.colors.contentDefault) on root view for the back chevron.
subtitle rendered on iOS 26+ only; ignored when hasLargeTitle: true.
Setup (bottom toolbar):
- Never combine with
OUDSTabBar on the same screen.
groupedItems layout meaningful on iOS 26+ only.
NavigationStack {
ContentView().toolBarTop("Title")
}
NavigationStack {
ContentView()
.toolBarTop("Title",
leadingItems: { OUDSToolBarItem(navigation: .back()) },
trailingItems: {
OUDSToolBarItem(icon: Image("ic_settings"), accessibilityLabel: "Settings") {}
})
}
ContentView().toolBarTop("Title", hasLargeTitle: true, subtitle: "Sub")
ContentView()
.toolBarBottom(
leadingItems: { OUDSToolBarItem(label: "Edit") {} },
trailingItems: { OUDSToolBarItem(icon: Image("ic_share"), accessibilityLabel: "Share") {} })
ContentView()
.toolBarBottom(groupedItems: {
OUDSToolBarItem(label: "Save") {}
OUDSToolBarItem(icon: Image("ic_delete"), accessibilityLabel: "Delete") {}
})
OUDSToolBarItem reference:
OUDSToolBarItem(label: "Edit") {}
OUDSToolBarItem(icon: Image("ic"), accessibilityLabel: "X") {}
OUDSToolBarItem(navigation: .back())
OUDSToolBarItem(navigation: .back(label: "Cancel"))
OUDSToolBarItem(navigation: .back(label: "Back") { saveDraft() })
OUDSToolBarItem(navigation: .close)
OUDSToolBarItem(action: .icon(asset: Image("ic_bell"), accessibilityLabel: "Notif",
badgeType: .standard))
OUDSToolBarItem(action: .icon(asset: Image("ic_mail"), accessibilityLabel: "Mail",
badgeType: .number(count: 9)))
if #available(iOS 26, *) {
OUDSToolBarItem(action: .label("Save", emphasized: false, accessibilityHint: nil) {},
style: .prominent)
}
OUDSToolBarItem { Menu("More") { Button("Option 1") {} } }
.toolBarTop("Title", trailingItems: {
if isEditing {
OUDSToolBarItem(label: "Done") { isEditing = false }
} else {
OUDSToolBarItem(label: "Edit") { isEditing = true }
}
})
Badge rendering: iOS ≤ 25 → OUDSBadge; iOS 26+ top → native system badge; iOS 26+ bottom → OUDSBadge forced.
Registering custom fonts
To use a custom font family with OUDS, two steps are required after adding the TTF files to your project:
Step 1 — Register the font files (Core Text, call once at app startup):
private static var fontsAlreadyRegistered = false
private func registerFonts() {
guard !Self.fontsAlreadyRegistered else { return }
Bundle.main.urls(forResourcesWithExtension: "ttf", subdirectory: nil)?
.forEach { CTFontManagerRegisterFontsForURL($0 as CFURL, .process, nil) }
Self.fontsAlreadyRegistered = true
}
Step 2 — Register PostScript names for each family + weight combination you use:
registerFont(postScript: "WinkyRough-Regular_Light", forCombination: PSFNMK("Winky Rough", Font.Weight.light))
registerFont(postScript: "WinkyRough-Regular", forCombination: PSFNMK("Winky Rough", Font.Weight.regular))
registerFont(postScript: "WinkyRough-Regular_Medium", forCombination: PSFNMK("Winky Rough", Font.Weight.medium))
registerFont(postScript: "WinkyRough-Regular_SemiBold",forCombination: PSFNMK("Winky Rough", Font.Weight.semibold))
registerFont(postScript: "WinkyRough-Regular_Bold", forCombination: PSFNMK("Winky Rough", Font.Weight.bold))
registerFont(postScript: "WinkyRough-Regular_Black", forCombination: PSFNMK("Winky Rough", Font.Weight.black))
kApplePostScriptFontNames exposes the full map (read-only). OUDS uses it internally to resolve Font objects from theme font tokens. Unregistered combinations fall back to the family name without spaces.