🌍 Idioma · Documentación canónica en inglés · English · Português (Brasil) · 日本語 · 简体中文 · 한국어 · Русский
Estandariza rápidamente texto, botones y superficies con un sistema de diseño unificado.
Text("Modificadores simples")
.gentleText(.title_xl)
Button("En toda tu aplicación") { }
.gentleButton(.primary)
VStack {
Text("Ahorra mucho tiempo")
}
.gentleSurface(.card)Aplicaciones que desean una base sólida en SwiftUI con evolución temática a largo plazo.
💬 Únete a la discusión. Comentarios y preguntas son bienvenidos
Véalo en acción: Abre Demo/GentleDesignSystemDemo.xcodeproj para explorar los componentes. La aplicación de demostración también permite editar y compartir especificaciones JSON mediante la hoja de compartir del sistema.
.package(url: "https://github.com/gentle-giraffe-apps/GentleDesignSystem.git", from: "0.1.7")import GentleDesignSystem
@main
struct MiApp: App {
var body: some Scene {
WindowGroup {
GentleThemeRoot(theme: .default) {
ContentView()
}
}
}
}Text("Bienvenido")
.gentleText(.title_xl)
Text("Descripción")
.gentleText(.body_m, colorRole: .textSecondary)Button("Continuar") { }
.gentleButton(.primary)
Button("Cancelar") { }
.gentleButton(.secondary)VStack {
Text("Contenido de la tarjeta")
}
.gentleSurface(.card)Detalles de CI, análisis estático y cobertura
Este proyecto aplica controles de calidad mediante CI y análisis estático:
- CI: Todos los commits a
maindeben pasar las verificaciones de GitHub Actions - Análisis estático: DeepSource se ejecuta en cada commit
- Cobertura de pruebas: Codecov reporta la cobertura de líneas
GentleDesignSystem está estructurado intencionalmente alrededor de tres capas:
- Definiciones de Tokens (Codable, compatible con JSON)
- Resolución en Tiempo de Ejecución (Tema + Environment)
- Ergonomía de SwiftUI (Modificadores y Extensiones)
Esta separación mantiene la intención de diseño clara, el comportamiento en tiempo de ejecución predecible y la evolución futura segura.
Arquitectura del Sistema (diagrama)
flowchart TB
subgraph Tokens["Capa de Tokens (Tiempo de Diseño)"]
Spec[GentleDesignSystemSpec]
Spec --> Colors[GentleColorTokens]
Spec --> Typography[GentleTypographyTokens]
Spec --> Layout[GentleLayoutTokens]
Spec --> Visual[GentleVisualTokens]
Spec --> Buttons[GentleButtonTokens]
Spec --> Surfaces[GentleSurfaceTokens]
end
subgraph Runtime["Capa de Tiempo de Ejecución"]
Theme[GentleTheme]
Manager[GentleThemeManager]
Store[GentleFileThemeSpecStore]
Manager --> Theme
Store -.->|cargar/guardar| Manager
end
subgraph SwiftUI["Capa de SwiftUI"]
Root[GentleThemeRoot]
Env[Environment Values .gentleTheme]
Modifiers[Modificadores de Vista]
Root --> Env
Env --> Modifiers
end
Tokens --> Runtime
Runtime --> SwiftUI
Flujo de Datos (diagrama)
flowchart TB
JSON[(Archivo JSON)] -->|cargar| Store[GentleFileThemeSpecStore]
Store --> Manager[GentleThemeManager]
Manager --> Theme[GentleTheme]
Theme --> Resolve{Resolución}
Resolve -->|ColorScheme| ResolvedColor[Color]
Resolve -->|ContentSizeCategory| ResolvedFont[Fuente]
ResolvedColor --> View[Vista SwiftUI]
ResolvedFont --> View
View -->|.gentleText| Text
View -->|.gentleButton| Button
View -->|.gentleSurface| Surface
Modelo de Datos (estructura de la especificación)
El sistema de diseño está definido por una única especificación compatible con JSON
(GentleDesignSystemSpec).
GentleDesignSystemSpec
│
├── colors: GentleColorTokens
│ │
│ └── pairByRole: [String: GentleColorPair]
│ │
│ ├── clave = GentleColorRole.rawValue
│ └── valor = GentleColorPair
│ ├── lightHex: String
│ └── darkHex: String
│
├── typography: GentleTypographyTokens
│ │
│ └── roles: [String: GentleTypographyRoleSpec]
│ │
│ ├── clave = GentleTextRole.rawValue
│ └── valor = GentleTypographyRoleSpec
│ ├── pointSize: Double
│ ├── weight: GentleFontWeightToken
│ ├── design: GentleFontDesignToken
│ ├── width: GentleFontWidthToken?
│ ├── relativeTo: GentleFontTextStyle
│ ├── lineSpacing: Double
│ ├── letterSpacing: Double
│ ├── isUppercased: Bool
│ └── colorRole: GentleColorRole
│
├── layout: GentleLayoutTokens
│ │
│ ├── scale: GentleSpacingScaleTokens
│ │ ├── xs / s / m / l / xl / xxl : Double
│ │ └── value(for: GentleSpacingToken) -> Double
│ │
│ ├── gap: GentleGapTokens
│ ├── grid: GentleGridSpacingTokens
│ ├── touch: GentleTouchTokens
│ │
│ └── inset: GentleInsetTokens
│ │
│ └── tokensByRole: [String: GentleAxisInsetTokens]
│ │
│ ├── clave = GentleInsetRole.rawValue
│ └── valor = GentleAxisInsetTokens
│ ├── horizontal: GentleSpacingToken
│ └── vertical: GentleSpacingToken
│
├── visual: GentleVisualTokens
│ │
│ ├── radii: GentleRadiusTokens
│ │ ├── small: Double
│ │ ├── medium: Double
│ │ ├── large: Double
│ │ └── pill: Double
│ │
│ └── shadows: GentleShadowTokens
│ ├── none: Double
│ ├── small: Double
│ └── medium: Double
│
├── buttons: GentleButtonTokens
│ │
│ ├── roles: [String: GentleButtonRoleSpec]
│ │ │
│ │ ├── clave = GentleButtonRole.rawValue
│ │ └── valor = GentleButtonRoleSpec
│ │ ├── shape: GentleButtonShape
│ │ ├── fillRole: GentleButtonFillRole
│ │ ├── borderRole: GentleButtonBorderRole
│ │ ├── animationRole: GentleButtonAnimationRole
│ │ ├── pressedScale: Double
│ │ ├── pressedOpacity: Double
│ │ └── usesNativeStyle: Bool
│ │
│ └── animations: [String: GentleButtonAnimationSpec]
│ │
│ ├── clave = GentleButtonAnimationRole.rawValue
│ └── valor = GentleButtonAnimationSpec
│ ├── pressedScale: Double
│ ├── pressedOpacity: Double
│ ├── duration: Double
│ ├── springResponse: Double
│ ├── springDamping: Double
│ └── springBlend: Double
│
└── surfaces: GentleSurfaceTokens
│
└── roles: [String: GentleSurfaceRoleSpec]
│
├── clave = GentleSurfaceRole.rawValue
└── valor = GentleSurfaceRoleSpec
├── backgroundStyle: GentleSurfaceBackgroundStyle
│ ├── .solid(colorRole: GentleColorRole)
│ ├── .material(material:, tintColorRole:, tintOpacity:)
│ └── .glass(fallbackMaterial:, fallbackColorRole:)
├── border: GentleColorPair
├── cornerRadius: Double
├── borderWidth: Double
├── shadowRadius: Double
├── shadowOpacity: Double
├── shadowOffsetX: Double
└── shadowOffsetY: Double
¿Por qué roles en lugar de valores directos? Los roles proporcionan identificadores estables que permiten que los temas evolucionen de manera segura con el tiempo. Las especificaciones pueden cambiar, los presets pueden intercambiarse y los valores pueden sobrescribirse sin romper los sitios de llamada o los temas serializados.
La capa de tokens define qué significa tu sistema de diseño — no cómo se renderiza.
| Categoría | Tipos |
|---|---|
| Tipografía | GentleTextRole, GentleTypographyRoleSpec, GentleTypographyTokens |
| Colores | GentleColorRole, GentleColorPair, GentleColorTokens |
| Layout | GentleLayoutTokens, GentleSpacingToken, GentleGapTokens, GentleInsetTokens |
| Visual | GentleVisualTokens, GentleRadiusTokens, GentleShadowTokens |
| Botones | GentleButtonRole, GentleButtonRoleSpec, GentleButtonTokens, GentleButtonAnimationRole |
| Superficies | GentleSurfaceRole, GentleSurfaceRoleSpec, GentleSurfaceTokens |
Garantías de tokens y especificación base
Todos los tokens son:
CodableSendable- Compatibles con JSON
Esto facilita:
- Persistir temas
- Cargar temas remotamente
- Compartir tokens entre plataformas más adelante
public struct GentleDesignSystemSpec: Codable, Sendable {
public var specVersion: String
public var colors: GentleColorTokens
public var typography: GentleTypographyTokens
public var layout: GentleLayoutTokens
public var visual: GentleVisualTokens
public var buttons: GentleButtonTokens
public var surfaces: GentleSurfaceTokens
}El tema predeterminado (.default) es simplemente una especificación concreta.
En tiempo de ejecución, los tokens se resuelven en valores reales de SwiftUI.
GentleTheme:
- Posee un
GentleDesignSystemSpec - Resuelve:
- Colores según
ColorScheme - Fuentes según
ContentSizeCategory(Tipo Dinámico)
- Colores según
@Environment(\.gentleTheme) var themeLa resolución de tipografía usa UIFontMetrics para escalar correctamente los tamaños de fuente personalizados mientras permanecen anclados a los estilos de texto semánticos de Apple.
Esto asegura:
- El escalado de accesibilidad funciona correctamente
- Los tamaños de punto personalizados permanecen proporcionales
- Los cambios futuros de Tipo Dinámico permanecen seguros
Ayudantes de acceso en tiempo de ejecución
// Acceder a valores de tema resueltos
@GentleDesignRuntime private var design
// Usar en la vista
design.color(.textPrimary) // Color para el esquema actual
design.layout.stack.regular // Valor CGFloat de espaciado
design.buttons // Tokens de botonesLos environments de SwiftUI fluyen de arriba hacia abajo.
Al envolver la raíz de tu aplicación con:
GentleThemeRoot {
ContentView()
}aseguras que:
- Todas las vistas hijas reciben el mismo tema
- Las previsualizaciones se comportan consistentemente
- Las sobrescrituras de tema son fáciles después (por escena, por funcionalidad, por previsualización)
GentleThemeRoot es intencionalmente ligero — solo inyecta un único valor de environment.
Esto evita:
- Singletons globales
- Estado estático
- Magia implícita
GentleDesignSystem expone APIs ergonómicas mientras mantiene la lógica centralizada.
Modificadores de texto
Text("Hola")
.gentleText(.headline_m)Internamente:
- Resuelve la tipografía mediante
GentleTheme - Aplica fuente, ancho, diseño, espaciado, color
- Respeta el Tipo Dinámico automáticamente
Superficies
VStack { ... }
.gentleSurface(.card)Las superficies aplican:
- Color de fondo
- Padding (cuando es apropiado)
- Radio de esquinas
- Bordes o sombras
La API basada en roles evita que los "números mágicos" se filtren en las vistas.
Botones
Button("Guardar") { }
.gentleButton(.primary)Los botones son:
- Estilizados mediante
ButtonStyle - Completamente controlados por el tema
- Soportan animaciones configurables
- Fácilmente extensibles para nuevos roles
Edición en tiempo de ejecución, persistencia y almacenes
GentleThemeManager
@main
struct MiApp: App {
@State private var manager = GentleThemeManager(theme: .default)
var body: some Scene {
WindowGroup {
GentleThemeRoot(theme: manager.theme) {
ContentView()
}
.environment(\.gentleThemeManager, manager)
}
}
}Usando el Manager
@GentleThemeManagerRuntime private var manager
// Guardar el tema actual en disco
try manager.save()
// Cargar tema persistido
try manager.load()
// Obtener bindings para edición
manager.typographyBinding(for: .body_m)
manager.colorBinding(for: .primaryCTA)Persistencia
GentleFileThemeSpecStore maneja la persistencia JSON en Application Support:
let store = GentleFileThemeSpecStore(fileName: "mi-tema.json")
let manager = GentleThemeManager(theme: .default, store: store)GentleDesignSystem incluye 9 presets de temas integrados, cada uno diseñado para diferentes casos de uso y estéticas.
Presets de temas disponibles
// Obtener todos los presets disponibles
let presets = GentleDesignSystemSpec.allPresets
// Cada preset proporciona:
// - name: Nombre para mostrar (ej., "Gentle Default")
// - summary: Eslogan breve
// - description: Explicación detallada
// - purpose: Cuándo usar este preset
// - systemImageString: Nombre del SF Symbol para la UI
// - spec: El GentleDesignSystemSpec real| Preset | Resumen | Mejor Para |
|---|---|---|
| Gentle Default | Base calmada y equilibrada | Punto de partida versátil con jerarquía limpia |
| Classic Tan | Cálido, atemporal con tonos terrosos | Apps que se benefician de calidez y herencia |
| Modern Gray | Elegante, minimalista con bases neutras | Apps de negocios donde la claridad es primordial |
| Soft Green | Fresco, natural con acentos calmantes | Bienestar, productividad, enfoque tranquilo |
| Editorial Paper | Refinado, inspirado en imprenta para lectura | Apps con mucho contenido, lectura de formato largo |
| Technical Blue | Preciso, confiable con toques azules | Herramientas de desarrollo, dashboards |
| Bold Orange | Vibrante, energético con fuerte presencia | Apps que motivan a la acción |
| Elegant Purple | Sofisticado, lujoso con tonos ricos | Apps de estilo de vida, creatividad, premium |
| Compact Mint | Denso, eficiente con acentos frescos | Interfaces ricas en datos |
// Aplicar un preset a tu theme manager
@GentleThemeManagerRuntime private var manager
// Encontrar y aplicar un preset
if let editorialPreset = GentleDesignSystemSpec.allPresets.first(where: { $0.name == "Editorial Paper" }) {
manager.theme.editableSpec = editorialPreset.spec
}La aplicación de demostración incluye un ThemePickerView que muestra todos los presets como tarjetas interactivas. Cada tarjeta previsualiza la tipografía y colores del preset usando el propio tema del preset:
Construyendo un selector de temas
ForEach(presets, id: \.name) { preset in
let previewTheme = GentleTheme(
defaultSpec: preset.spec,
editableSpec: preset.spec
)
Button {
themeManager.theme.editableSpec = preset.spec
} label: {
GentleThemeRoot(theme: previewTheme) {
// El contenido de la tarjeta se renderiza con el estilo propio del preset
ThemePresetCard(preset: preset)
}
}
}Roles de tipografía
17 roles de texto semánticos organizados por rampa de tamaño (xxl > xl > l > ml > m > ms > s):
| Rampa | Roles |
|---|---|
| XXL | largeTitle_xxl |
| XL | title_xl |
| L | title2_l |
| ML | title3_ml |
| M | headline_m, body_m, bodySecondary_m, monoCode_m, primaryButtonTitle_m, secondaryButtonTitle_m, tertiaryButtonTitle_m, quaternaryButtonTitle_m |
| MS | callout_ms, subheadline_ms |
| S | footnote_s, caption_s, caption2_s |
Cada rol se resuelve a un GentleTypographyRoleSpec que contiene: pointSize, weight, design, width, relativeTo, lineSpacing, letterSpacing, isUppercased y colorRole.
Roles de botones y animaciones
Roles de Botones
primary · secondary · tertiary · quaternary · destructive
Animaciones de Botones
| Animación | Descripción |
|---|---|
unknown |
Sin animación |
subtlePress |
Retroalimentación sutil al presionar |
squish |
Efecto de aplastamiento al presionar |
pop |
Efecto de salto |
bouncy |
Animación de resorte rebotante |
springBack |
Se encoge al presionar, rebota más allá del tamaño original antes de estabilizarse |
Roles de superficies
`appBackground` · `card` · `cardElevated` · `cardSecondary` · `chrome` · `overlaySheet` · `overlayPopover` · `overlayScrim` · `floatingPanel` · `floatingWidget`Roles de colores
| Categoría | Roles |
|---|---|
| Texto (9) | textPrimary, textSecondary, textTertiary, textOnPrimaryCTA, textOnDestructive, textOnOverlay, textOnOverlaySecondary, textOnScrim, textOnScrimSecondary |
| Superficies (6) | background, surfaceBase, surfaceCardSecondary, surfaceTint, surfaceScrim, borderSubtle |
| Acciones (2) | primaryCTA, destructive |
| Tema (2) | themePrimary, themeSecondary |
Usa agrupaciones semánticas: GentleColorRole.textRoles, .surfaceRoles, .actionRoles, .themeRoles
Usa verificaciones de membresía: role.isTextRole, .isSurfaceRole, .isActionRole, .isThemeRole
Tokens de espaciado y radio
Tokens de Espaciado
xs (4) · s (8) · m (12) · l (16) · xl (24) · xxl (32)
Tokens de Radio
small (8) · medium (12) · large (20) · pill (999)
- iOS 18.0+
- Swift 6.1+
Partes de la redacción y refinamiento editorial en este repositorio fueron aceleradas usando modelos de lenguaje grandes (incluyendo ChatGPT, Claude y Gemini) bajo diseño humano directo, validación y aprobación final. Todas las decisiones técnicas, código y conclusiones arquitectónicas son autoría y verificación del mantenedor del repositorio.



