mirror of
https://github.com/phranck/TUIkit.git
synced 2026-06-20 09:54:37 +00:00
Add complete Apple-style DocC documentation for TUIKit framework: ## Documentation Structure - Main landing page (TUIKit.md) with quick-start and topic navigation - 8 guide articles: GettingStarted, ViewHierarchy, StateManagement, Theming, Appearance, Focus, Modifiers, Architecture - 3 step-by-step tutorials: BuildYourFirstApp, BuildInteractiveMenu, BuildThemableUI - Documentation catalog with proper Info.plist configuration ## Code Improvements - Enhanced Box.swift documentation with clear container comparison - Added comprehensive examples and usage patterns - Explained appearance integration and sizing behavior ## Infrastructure - GitHub Actions workflow (.github/workflows/docc.yml) for automatic DocC building - Configured for deployment to GitHub Pages on push to main - Uses xcrun docc build with proper artifact handling ## Documentation Includes - API reference stubs for all major components - Comprehensive guide articles with code examples - Interactive tutorials following Apple's documentation style - Cross-references using DocC syntax (``Type``) - Best practices and design patterns explained The documentation is ready for automated building and deployment to GitHub Pages.
422 lines
13 KiB
Markdown
422 lines
13 KiB
Markdown
# Building a Themable UI
|
|
|
|
Create an application with dynamic theme switching and persistence.
|
|
|
|
@Intro(title: "Build a Themable App") {
|
|
Learn how to integrate TUIKit's theming system into your application,
|
|
allowing users to switch between themes and persist their preference.
|
|
|
|
You'll work with the theme environment, theme manager, and storage to
|
|
create a professional themable application.
|
|
}
|
|
|
|
## Overview
|
|
|
|
In this tutorial, you'll create an app that features:
|
|
|
|
- Theme environment integration
|
|
- Dynamic theme switching
|
|
- Persisted theme preference
|
|
- Visual theme selection UI
|
|
|
|
@Section(title: "Set Up Theme Storage") {
|
|
@ContentAndMedia {
|
|
Use `@AppStorage` to remember the user's theme choice across sessions.
|
|
}
|
|
|
|
@Steps {
|
|
@Step {
|
|
Create an app that stores the selected theme:
|
|
|
|
```swift
|
|
import TUIKit
|
|
|
|
@main
|
|
struct ThemableApp: App {
|
|
@AppStorage("selectedTheme") var selectedThemeName: String = "green"
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
.environment(\.theme, themeForName(selectedThemeName))
|
|
}
|
|
}
|
|
|
|
func themeForName(_ name: String) -> Theme {
|
|
switch name {
|
|
case "amber":
|
|
return AmberPhosphorTheme()
|
|
case "white":
|
|
return WhitePhosphorTheme()
|
|
case "red":
|
|
return RedPhosphorTheme()
|
|
default:
|
|
return GreenPhosphorTheme()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ContentView: View {
|
|
var body: some View {
|
|
VStack(spacing: 2) {
|
|
Text("Themable Application")
|
|
.bold()
|
|
.foregroundColor(.theme.accent)
|
|
|
|
Spacer()
|
|
|
|
Text("Select a theme to customize the appearance")
|
|
.foregroundColor(.theme.foregroundSecondary)
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
```
|
|
|
|
`@AppStorage("selectedTheme")` automatically persists the theme choice.
|
|
When the app restarts, it loads the saved theme.
|
|
}
|
|
|
|
@Step {
|
|
Run your app:
|
|
|
|
```bash
|
|
swift run
|
|
```
|
|
|
|
The app launches with the default Green theme.
|
|
}
|
|
}
|
|
}
|
|
|
|
@Section(title: "Create Theme Selection UI") {
|
|
@ContentAndMedia {
|
|
Display the 4 available themes and let users select one.
|
|
}
|
|
|
|
@Steps {
|
|
@Step {
|
|
Add a theme menu:
|
|
|
|
```swift
|
|
@main
|
|
struct ThemableApp: App {
|
|
@AppStorage("selectedTheme") var selectedThemeName: String = "green"
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView(selectedThemeName: $selectedThemeName)
|
|
.environment(\.theme, themeForName(selectedThemeName))
|
|
}
|
|
}
|
|
|
|
func themeForName(_ name: String) -> Theme {
|
|
// ... same as before
|
|
}
|
|
}
|
|
|
|
struct ContentView: View {
|
|
@Binding var selectedThemeName: String
|
|
|
|
let themes = [
|
|
("green", "🟢 Green (Default)"),
|
|
("amber", "🟡 Amber"),
|
|
("white", "⚪ White"),
|
|
("red", "🔴 Red")
|
|
]
|
|
|
|
var body: some View {
|
|
VStack(spacing: 2) {
|
|
Text("Themable Application")
|
|
.bold()
|
|
.foregroundColor(.theme.accent)
|
|
|
|
Spacer()
|
|
|
|
Text("Select a theme:")
|
|
.bold()
|
|
|
|
Menu(
|
|
items: themes.map { id, label in
|
|
MenuItem(label, id: id)
|
|
},
|
|
selection: $selectedThemeName
|
|
)
|
|
|
|
Spacer()
|
|
|
|
Text("Current: \(themeName(selectedThemeName))")
|
|
.foregroundColor(.theme.foregroundSecondary)
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
func themeName(_ id: String) -> String {
|
|
themes.first(where: { $0.0 == id })?.1 ?? id
|
|
}
|
|
}
|
|
```
|
|
|
|
Pass `$selectedThemeName` as a binding to allow the menu to change it.
|
|
The environment automatically updates because it depends on this value.
|
|
}
|
|
|
|
@Step {
|
|
Test theme switching:
|
|
|
|
```bash
|
|
swift run
|
|
```
|
|
|
|
Use Tab/Arrow keys to select different themes.
|
|
Notice the colors change immediately!
|
|
}
|
|
}
|
|
}
|
|
|
|
@Section(title: "Display Theme Colors") {
|
|
@ContentAndMedia {
|
|
Show a visual preview of the current theme's colors.
|
|
}
|
|
|
|
@Steps {
|
|
@Step {
|
|
Add a color preview:
|
|
|
|
```swift
|
|
struct ContentView: View {
|
|
@Binding var selectedThemeName: String
|
|
@Environment(\.theme) var theme
|
|
|
|
let themes = [
|
|
("green", "🟢 Green (Default)"),
|
|
("amber", "🟡 Amber"),
|
|
("white", "⚪ White"),
|
|
("red", "🔴 Red")
|
|
]
|
|
|
|
var body: some View {
|
|
VStack(spacing: 2) {
|
|
Text("Themable Application")
|
|
.bold()
|
|
.foregroundColor(.theme.accent)
|
|
|
|
Spacer()
|
|
|
|
Text("Select a theme:").bold()
|
|
|
|
Menu(
|
|
items: themes.map { id, label in
|
|
MenuItem(label, id: id)
|
|
},
|
|
selection: $selectedThemeName
|
|
)
|
|
|
|
Spacer()
|
|
|
|
// Color preview box
|
|
VStack(spacing: 1) {
|
|
Text("Theme Colors")
|
|
.bold()
|
|
|
|
HStack(spacing: 1) {
|
|
Text(" ").background(theme.foreground)
|
|
Text(" ").background(theme.accent)
|
|
Text(" ").background(theme.border)
|
|
Text(" ").background(theme.warning)
|
|
}
|
|
|
|
Text("Foreground | Accent | Border | Warning")
|
|
.foregroundColor(.theme.foregroundSecondary)
|
|
}
|
|
.padding(1)
|
|
.border(.rounded)
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
func themeName(_ id: String) -> String {
|
|
themes.first(where: { $0.0 == id })?.1 ?? id
|
|
}
|
|
}
|
|
```
|
|
|
|
`@Environment(\.theme)` gives access to the current theme.
|
|
Use `theme.foreground`, `theme.accent`, etc. to access colors.
|
|
}
|
|
|
|
@Step {
|
|
Run the app:
|
|
|
|
```bash
|
|
swift run
|
|
```
|
|
|
|
Switch themes and see the color preview update.
|
|
}
|
|
}
|
|
}
|
|
|
|
@Section(title: "Add Theme Shortcuts and Status Bar") {
|
|
@ContentAndMedia {
|
|
Let users cycle themes quickly with keyboard shortcuts.
|
|
}
|
|
|
|
@Steps {
|
|
@Step {
|
|
Add quick theme cycling:
|
|
|
|
```swift
|
|
struct ContentView: View {
|
|
@Binding var selectedThemeName: String
|
|
@Environment(\.theme) var theme
|
|
|
|
let themes = [
|
|
("green", "🟢 Green (Default)"),
|
|
("amber", "🟡 Amber"),
|
|
("white", "⚪ White"),
|
|
("red", "🔴 Red")
|
|
]
|
|
|
|
let themeIds = ["green", "amber", "white", "red"]
|
|
|
|
var body: some View {
|
|
VStack(spacing: 2) {
|
|
// ... menu UI as before ...
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.onKeyPress { event in
|
|
if event.key == .character("t") {
|
|
// Cycle to next theme
|
|
if let currentIndex = themeIds.firstIndex(of: selectedThemeName) {
|
|
let nextIndex = (currentIndex + 1) % themeIds.count
|
|
selectedThemeName = themeIds[nextIndex]
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
.statusBarItems {
|
|
StatusBarItem(
|
|
label: "Cycle Theme",
|
|
shortcut: Shortcut.letter("t"),
|
|
action: { }
|
|
)
|
|
|
|
StatusBarItem(
|
|
label: "Help",
|
|
shortcut: Shortcut.letter("?"),
|
|
action: { }
|
|
)
|
|
|
|
StatusBarItem(
|
|
label: "Quit",
|
|
shortcut: Shortcut.letter("q"),
|
|
action: { }
|
|
)
|
|
}
|
|
}
|
|
|
|
func themeName(_ id: String) -> String {
|
|
themes.first(where: { $0.0 == id })?.1 ?? id
|
|
}
|
|
}
|
|
```
|
|
|
|
Now users can press `t` to quickly cycle through themes.
|
|
}
|
|
|
|
@Step {
|
|
Test theme cycling:
|
|
|
|
```bash
|
|
swift run
|
|
```
|
|
|
|
Press `t` repeatedly to cycle through all 4 themes.
|
|
The selection updates and the preview colors change.
|
|
}
|
|
}
|
|
}
|
|
|
|
@Section(title: "Enhance with Appearance Switching") {
|
|
@ContentAndMedia {
|
|
Add appearance style switching alongside theme switching.
|
|
}
|
|
|
|
@Steps {
|
|
@Step {
|
|
Add appearance cycling:
|
|
|
|
```swift
|
|
@main
|
|
struct ThemableApp: App {
|
|
@AppStorage("selectedTheme") var selectedThemeName: String = "green"
|
|
@AppStorage("selectedAppearance") var selectedAppearance: String = "rounded"
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView(
|
|
selectedThemeName: $selectedThemeName,
|
|
selectedAppearance: $selectedAppearance
|
|
)
|
|
.environment(\.theme, themeForName(selectedThemeName))
|
|
.environment(\.appearance, appearanceForName(selectedAppearance))
|
|
}
|
|
}
|
|
|
|
func themeForName(_ name: String) -> Theme {
|
|
// ... existing code ...
|
|
}
|
|
|
|
func appearanceForName(_ name: String) -> Appearance {
|
|
switch name {
|
|
case "line":
|
|
return .line
|
|
case "doubled":
|
|
return .doubleLine
|
|
case "heavy":
|
|
return .heavy
|
|
case "block":
|
|
return .block
|
|
default:
|
|
return .rounded
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Now users can customize both theme AND appearance style!
|
|
}
|
|
|
|
@Step {
|
|
The changes are automatically persisted with `@AppStorage`.
|
|
Run the app:
|
|
|
|
```bash
|
|
swift run
|
|
```
|
|
|
|
Select a theme and appearance, exit with `q`, and run again.
|
|
Your selection is remembered!
|
|
}
|
|
}
|
|
}
|
|
|
|
## Next Steps
|
|
|
|
Congratulations! You've built a fully themed, customizable application.
|
|
|
|
- Learn more about <doc:Theming> for creating custom themes
|
|
- Explore <doc:Appearance> for structural styles
|
|
- See <doc:StateManagement> for advanced state patterns
|
|
- Check the <doc:Architecture> overview for deeper understanding
|