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 // Always use the umbrella import
@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
Never import internal modules. Always use an umbrella product:
| Product | Themes | When |
|---|---|---|
OUDSSwiftUI |
All | Default |
OUDSSwiftUIOrange |
Orange + OrangeCompact | Orange apps only |
OUDSSwiftUIOrangeSosh |
Orange + Sosh | Multi-brand |
OUDSSwiftUIWireframe |
Wireframe | Prototyping |
Internal modules (never import directly): OUDSComponents, OUDSTokensRaw, OUDSTokensSemantic, OUDSTokensComponent, OUDSThemesOrange, OUDSThemesSosh, OUDSFoundations, etc.
3. Themes
OUDSThemeableView(theme: OrangeTheme()) { … } // inject at root
@Environment(\.theme) private var theme // consume anywhere
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) // typography — use view modifier with token
.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) // shape fill with token
RoundedRectangle(cornerRadius: 8)
.fill(theme.colors.bgPrimary, style: FillStyle(eoFill: true)) // with custom fill style
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",
icon: Image(systemName: "plus").accessibilityHidden(true), // ❌ compile error: Image → some View
appearance: .default) {}
OUDSLink(
text: "Back",
icon: Image(systemName: "chevron.left").accessibilityHidden(true), // ❌ same error
size: .default) {}
Always do this — swiftlint comment on the line before the component call:
// swiftlint:disable:next accessibility_label_for_image
OUDSButton(text: "Add", icon: Image(systemName: "plus"), appearance: .default) {}
// swiftlint:disable:next accessibility_label_for_image
OUDSLink(text: "Back", icon: Image(systemName: "chevron.left"), size: .default) {}
// swiftlint:disable:next accessibility_label_for_image
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", icon: Image("ic"), appearance: .default) {}
OUDSButton(icon: 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", icon: Image(decorative: "ic"))
OUDSCheckboxItem("Label", isOn: $isOn, icon: Image(decorative: "ic"), isReversed: true)
// Error / helper / disabled → see §6 Common patterns
Parameter order:
(_ label:, isOn:, description:, icon:, flipIcon:, isReversed:, isError:, errorText:, isReadOnly:, hasDivider:, constrainedMaxWidth:, action:)
// Picker
OUDSCheckboxPicker(selections: $selections, checkboxes: [
.init(tag: "a", label: "Option A"),
.init(tag: "b", label: "Option B", description: "Details", isReversed: true),
])
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, icon: Image(decorative: "ic"))
// Error / helper / disabled → see §6 Common patterns
OUDSRadioPicker(selection: $selection,
radios: [.init(tag: "a", label: "Option A")],
placement: .vertical)
Controls — Switch
OUDSSwitch(isOn: $isOn, accessibilityLabel: "Label")
OUDSSwitchItem("Label", isOn: $isOn)
OUDSSwitchItem("Label", isOn: $isOn, icon: Image(decorative: "ic"))
// Error / helper / disabled → see §6 Common patterns
Controls — Pin Code Input
Available lengths: .four, .six (default).
OUDSPinCodeInput($value)
OUDSPinCodeInput($value, length: .four, autofocus: true)
OUDSPinCodeInput($value, helperText: "Enter your PIN")
// Error status → see §6 Common patterns (status: .error / .richError)
Controls — Password Input
statusis of typeOUDSTextInput.Status(shared withOUDSTextInput).
OUDSPasswordInput(label: "Password", password: $password, isHiddenPassword: $isHidden)
OUDSPasswordInput(label: "Password", password: $password, isHiddenPassword: $isHidden,
placeholder: "Min. 8 chars", prefix: "🔑", lockIcon: true)
// Helper / error status → see §6 Common patterns
Controls — Chips
OUDSSuggestionChip(text: "Label") {}
OUDSSuggestionChip(icon: Image("ic"), text: "Label") {}
OUDSFilterChip(text: "Label") {}
OUDSFilterChip(icon: Image("ic"), text: "Label") {}
OUDSChipPicker(title: "Title", selection: $selection, chips: [
.init(tag: .value1, layout: .textAndIcon("Label", icon: Image("ic"))),
])
Controls — Text Input
OUDSTextInput(label: "Label", text: $text)
OUDSTextInput(label: "Label", text: $text, placeholder: "…", prefix: "Pre", suffix: "Suf")
OUDSTextInput(label: "Label", text: $text, leadingIcon: Image("ic"))
OUDSTextInput(label: "Label", text: $text,
trailingAction: .init(icon: Image("ic"), actionHint: "Hint") {})
// Helper / error status → see §6 Common patterns
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) })
// Fixed height — no vertical growth, scroll from first overflow line
OUDSTextArea(label: "Label", text: $text, constrainedMaxHeight: true)
// Error status → see §6 Common patterns
Height is controlled by two component tokens on
theme.textArea:
sizeMinHeightInput(72 pt by default) — minimum height, always appliedsizeMaxHeightInput(240 pt by default) — maximum height before scroll (used whenconstrainedMaxHeight: false, the default)When
constrainedMaxHeight: true,maxHeightis capped tosizeMinHeightInput, 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") { /* dismiss */ }
OUDSAlertMessage(label: "Label",
status: .neutral(icon: OUDSIcon(asset: Image("ic_heart"))),
bulletList: ["A", "B"],
link: .init(text: "More", position: .bottom) {},
onClose: {})
// Rich description / rich bullet list: pass AttributedString instead of String
Dialogs — Inline Alert
Statuses: neutral, accent, positive, info, warning, negative
OUDSInlineAlert(label: "Label")
OUDSInlineAlert(label: "Label", status: .warning)
OUDSInlineAlert(label: "Label", status: .accent(icon: OUDSIcon(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(icon: Image("ic")), accessibilityLabel: "Label", size: .medium)
Indicators — Tag
OUDSTag(label: "Label")
OUDSTag(label: "Label", status: .neutral(icon: Image("ic")))
OUDSTag(label: "Label", status: .neutral(bullet: true))
Indicators — Input Tag
OUDSInputTag("Label") { /* remove action */ }
Layouts — Colored Surface
OUDSColoredSurface(color: theme.colorModes.onStatusPositiveEmphasized) {
// child views
}
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", icon: Image("ic"), size: .default) {}
Navigations — Tab Bar
Never combine with
OUDSToolBarBottomon the same screen.
// iOS 15–25
@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)
}
// iOS 26+
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:)(plainInt) is deprecated — useselectedTab: 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 rootNavigationStack. - On iOS ≤ 18: add
.accentColor(theme.colors.contentDefault)on root view for the back chevron. subtitlerendered on iOS 26+ only; ignored whenhasLargeTitle: true.
Setup (bottom toolbar):
- Never combine with
OUDSTabBaron the same screen. groupedItemslayout meaningful on iOS 26+ only.
// Top — minimal
NavigationStack {
ContentView().toolBarTop("Title")
}
// Top — with items
NavigationStack {
ContentView()
.toolBarTop("Title",
leadingItems: { OUDSToolBarItem(navigation: .back()) },
trailingItems: {
OUDSToolBarItem(icon: Image("ic_settings"), accessibilityLabel: "Settings") {}
})
}
// Top — large title + subtitle (subtitle iOS 26+ only)
ContentView().toolBarTop("Title", hasLargeTitle: true, subtitle: "Sub")
// Bottom — leading/trailing split
ContentView()
.toolBarBottom(
leadingItems: { OUDSToolBarItem(label: "Edit") {} },
trailingItems: { OUDSToolBarItem(icon: Image("ic_share"), accessibilityLabel: "Share") {} })
// Bottom — grouped (iOS 26+ only)
ContentView()
.toolBarBottom(groupedItems: {
OUDSToolBarItem(label: "Save") {}
OUDSToolBarItem(icon: Image("ic_delete"), accessibilityLabel: "Delete") {}
})
OUDSToolBarItem reference:
OUDSToolBarItem(label: "Edit") {} // text action
OUDSToolBarItem(icon: Image("ic"), accessibilityLabel: "X") {} // icon action
OUDSToolBarItem(navigation: .back()) // back — auto dismiss
OUDSToolBarItem(navigation: .back(label: "Cancel")) // back with label (ignored iOS 26+)
OUDSToolBarItem(navigation: .back(label: "Back") { saveDraft() }) // back + custom action
OUDSToolBarItem(navigation: .close) // close — NO closure, auto dismiss
// Badge on icon (v2.0.0+) — icon actions only
OUDSToolBarItem(action: .icon(asset: Image("ic_bell"), accessibilityLabel: "Notif",
badgeType: .standard))
OUDSToolBarItem(action: .icon(asset: Image("ic_mail"), accessibilityLabel: "Mail",
badgeType: .number(count: 9)))
// Action style — iOS 26+ only
if #available(iOS 26, *) {
OUDSToolBarItem(action: .label("Save", emphasized: false, accessibilityHint: nil) {},
style: .prominent) // .default | .prominent | .tinted
}
// Custom view
OUDSToolBarItem { Menu("More") { Button("Option 1") {} } }
// Conditional (result-builder syntax)
.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 →OUDSBadgeforced.
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.