Skip to content

Latest commit

 

History

History
696 lines (531 loc) · 27.5 KB

File metadata and controls

696 lines (531 loc) · 27.5 KB

CI Coverage Swift iOS SPM DeepSource Static Analysis DeepSource Commit activity Last commit

🌍 Язык · Каноническая документация на английском · English · Español · Português (Brasil) · 日本語 · 简体中文 · 한국어

Кратко

Быстро стандартизируйте текст, кнопки и поверхности с помощью единой дизайн-системы.

Text("Простые модификаторы")
    .gentleText(.title_xl)

Button("По всему приложению") { }
    .gentleButton(.primary)

VStack {
    Text("Экономьте массу времени")
}
.gentleSurface(.card)

Для кого

Приложения, которым нужна надёжная основа на SwiftUI с долгосрочным развитием тем.

💬 Присоединяйтесь к обсуждению. Отзывы и вопросы приветствуются

Посмотрите в действии: Откройте Demo/GentleDesignSystemDemo.xcodeproj для изучения компонентов. Демо-приложение также поддерживает редактирование и обмен JSON-спецификациями через системный лист общего доступа.


Быстрый старт

1. Добавьте пакет

.package(url: "https://github.com/gentle-giraffe-apps/GentleDesignSystem.git", from: "0.1.7")

2. Оберните корень приложения

import GentleDesignSystem

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            GentleThemeRoot(theme: .default) {
                ContentView()
            }
        }
    }
}

3. Используйте компоненты

Типографика

Text("Добро пожаловать")
    .gentleText(.title_xl)

Text("Описание")
    .gentleText(.body_m, colorRole: .textSecondary)

Кнопки

Button("Продолжить") { }
    .gentleButton(.primary)

Button("Отмена") { }
    .gentleButton(.secondary)

Поверхности

VStack {
    Text("Содержимое карточки")
}
.gentleSurface(.card)

Качество и инструменты

Подробности CI, статического анализа и покрытия

Проект обеспечивает контроль качества через CI и статический анализ:

  • CI: Все коммиты в main должны пройти проверки GitHub Actions
  • Статический анализ: DeepSource запускается при каждом коммите
  • Покрытие тестами: Codecov отчитывается о покрытии строк

Снимок Codecov


Обзор архитектуры

GentleDesignSystem намеренно построен вокруг трёх слоёв:

  1. Определения токенов (Codable, совместимые с JSON)
  2. Разрешение в рантайме (Тема + Environment)
  3. Эргономика SwiftUI (Модификаторы и расширения)

Такое разделение сохраняет ясность дизайн-замысла, предсказуемость поведения во время выполнения и безопасность будущего развития.

Архитектура системы (диаграмма)
flowchart TB
    subgraph Tokens["Слой токенов (время проектирования)"]
        Spec[GentleDesignSystemSpec]
        Spec --> Colors[GentleColorTokens]
        Spec --> Typography[GentleTypographyTokens]
        Spec --> Layout[GentleLayoutTokens]
        Spec --> Visual[GentleVisualTokens]
        Spec --> Buttons[GentleButtonTokens]
        Spec --> Surfaces[GentleSurfaceTokens]
    end

    subgraph Runtime["Слой рантайма"]
        Theme[GentleTheme]
        Manager[GentleThemeManager]
        Store[GentleFileThemeSpecStore]
        Manager --> Theme
        Store -.->|загрузка/сохранение| Manager
    end

    subgraph SwiftUI["Слой SwiftUI"]
        Root[GentleThemeRoot]
        Env[Environment Values .gentleTheme]
        Modifiers[Модификаторы представлений]
        Root --> Env
        Env --> Modifiers
    end

    Tokens --> Runtime
    Runtime --> SwiftUI
Loading
Поток данных (диаграмма)
flowchart TB
    JSON[(JSON-файл)] -->|загрузка| Store[GentleFileThemeSpecStore]
    Store --> Manager[GentleThemeManager]
    Manager --> Theme[GentleTheme]
    Theme --> Resolve{Разрешение}

    Resolve -->|ColorScheme| ResolvedColor[Цвет]
    Resolve -->|ContentSizeCategory| ResolvedFont[Шрифт]

    ResolvedColor --> View[Представление SwiftUI]
    ResolvedFont --> View

    View -->|.gentleText| Text
    View -->|.gentleButton| Button
    View -->|.gentleSurface| Surface
Loading
Модель данных (структура спецификации)

Дизайн-система определяется единой JSON-совместимой спецификацией (GentleDesignSystemSpec).

GentleDesignSystemSpec
│
├── colors: GentleColorTokens
│       │
│       └── pairByRole: [String: GentleColorPair]
│               │
│               ├── ключ = GentleColorRole.rawValue
│               └── значение = GentleColorPair
│                       ├── lightHex: String
│                       └── darkHex:  String
│
├── typography: GentleTypographyTokens
│       │
│       └── roles: [String: GentleTypographyRoleSpec]
│               │
│               ├── ключ = GentleTextRole.rawValue
│               └── значение = 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]
│                       │
│                       ├── ключ = GentleInsetRole.rawValue
│                       └── значение = 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]
│       │       │
│       │       ├── ключ = GentleButtonRole.rawValue
│       │       └── значение = GentleButtonRoleSpec
│       │               ├── shape: GentleButtonShape
│       │               ├── fillRole: GentleButtonFillRole
│       │               ├── borderRole: GentleButtonBorderRole
│       │               ├── animationRole: GentleButtonAnimationRole
│       │               ├── pressedScale: Double
│       │               ├── pressedOpacity: Double
│       │               └── usesNativeStyle: Bool
│       │
│       └── animations: [String: GentleButtonAnimationSpec]
│               │
│               ├── ключ = GentleButtonAnimationRole.rawValue
│               └── значение = GentleButtonAnimationSpec
│                       ├── pressedScale: Double
│                       ├── pressedOpacity: Double
│                       ├── duration: Double
│                       ├── springResponse: Double
│                       ├── springDamping: Double
│                       └── springBlend: Double
│
└── surfaces: GentleSurfaceTokens
      │
      └── roles: [String: GentleSurfaceRoleSpec]
              │
              ├── ключ = GentleSurfaceRole.rawValue
              └── значение = 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

Почему роли, а не прямые значения? Роли предоставляют стабильные идентификаторы, позволяющие темам безопасно развиваться со временем. Спецификации могут меняться, пресеты — подменяться, значения — переопределяться, не нарушая при этом точки вызова или сериализованные темы.


1. Слой токенов (время проектирования)

Слой токенов определяет, что означает ваша дизайн-система, а не как она отрисовывается.

Категории токенов

Категория Типы
Типографика GentleTextRole, GentleTypographyRoleSpec, GentleTypographyTokens
Цвета GentleColorRole, GentleColorPair, GentleColorTokens
Раскладка GentleLayoutTokens, GentleSpacingToken, GentleGapTokens, GentleInsetTokens
Визуал GentleVisualTokens, GentleRadiusTokens, GentleShadowTokens
Кнопки GentleButtonRole, GentleButtonRoleSpec, GentleButtonTokens, GentleButtonAnimationRole
Поверхности GentleSurfaceRole, GentleSurfaceRoleSpec, GentleSurfaceTokens
Гарантии токенов и базовая спецификация

Все токены:

  • Codable
  • Sendable
  • Совместимы с JSON

Это упрощает:

  • Сохранение тем
  • Удалённую загрузку тем
  • Совместное использование токенов на разных платформах в будущем
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
}

Тема по умолчанию (.default) — это просто одна конкретная спецификация.


2. Слой рантайма (разрешение темы)

Во время выполнения токены разрешаются в реальные значения SwiftUI.

GentleTheme

GentleTheme:

  • Владеет GentleDesignSystemSpec
  • Разрешает:
    • Цвета по ColorScheme
    • Шрифты по ContentSizeCategory (Dynamic Type)
@Environment(\.gentleTheme) var theme

Разрешение типографики использует UIFontMetrics для корректного масштабирования пользовательских размеров шрифтов, оставаясь привязанным к семантическим текстовым стилям Apple.

Это гарантирует:

  • Корректную работу масштабирования для доступности
  • Сохранение пропорциональности пользовательских размеров
  • Безопасность при будущих изменениях Dynamic Type

Property Wrappers

Вспомогательные средства доступа в рантайме
// Доступ к разрешённым значениям темы
@GentleDesignRuntime private var design

// Использование в представлении
design.color(.textPrimary)    // Цвет для текущей схемы
design.layout.stack.regular   // Значение CGFloat для отступа
design.buttons                // Токены кнопок

3. Внедрение Environment

Зачем нужен GentleThemeRoot

Environment в SwiftUI распространяется сверху вниз.

Обернув корень приложения:

GentleThemeRoot {
    ContentView()
}

вы гарантируете, что:

  • Все дочерние представления получают одну и ту же тему
  • Превью ведут себя единообразно
  • Переопределение тем легко (по сцене, по функции, по превью)

GentleThemeRoot намеренно легковесен — он внедряет только одно значение environment.

Это исключает:

  • Глобальные синглтоны
  • Статическое состояние
  • Неявную магию

4. Модификаторы и расширения представлений

GentleDesignSystem предоставляет эргономичные API, сохраняя логику централизованной.

Текстовые модификаторы
Text("Привет")
  .gentleText(.headline_m)

Внутри:

  • Типографика разрешается через GentleTheme
  • Применяются шрифт, ширина, дизайн, интервалы, цвет
  • Автоматически поддерживается Dynamic Type
Поверхности
VStack { ... }
  .gentleSurface(.card)

Поверхности применяют:

  • Цвет фона
  • Отступы (при необходимости)
  • Скругление углов
  • Рамки или тени

API на основе ролей предотвращает утечку «магических чисел» в представления.

Кнопки
Button("Сохранить") { }
  .gentleButton(.primary)

Кнопки:

  • Стилизуются через ButtonStyle
  • Полностью управляются темой
  • Поддерживают настраиваемые анимации
  • Легко расширяются новыми ролями

5. Управление темами и сохранение

Редактирование в рантайме, сохранение и хранилища

GentleThemeManager

@main
struct MyApp: App {
  @State private var manager = GentleThemeManager(theme: .default)

  var body: some Scene {
      WindowGroup {
          GentleThemeRoot(theme: manager.theme) {
              ContentView()
          }
          .environment(\.gentleThemeManager, manager)
      }
  }
}

Использование Manager

@GentleThemeManagerRuntime private var manager

// Сохранить текущую тему на диск
try manager.save()

// Загрузить сохранённую тему
try manager.load()

// Получить привязки для редактирования
manager.typographyBinding(for: .body_m)
manager.colorBinding(for: .primaryCTA)

Сохранение

GentleFileThemeSpecStore обрабатывает JSON-сохранение в Application Support:

let store = GentleFileThemeSpecStore(fileName: "my-theme.json")
let manager = GentleThemeManager(theme: .default, store: store)

6. Пресеты тем

GentleDesignSystem включает 9 встроенных пресетов тем, каждый из которых спроектирован для разных сценариев и эстетики.

Доступные пресеты тем
// Получить все доступные пресеты
let presets = GentleDesignSystemSpec.allPresets

// Каждый пресет предоставляет:
// - name: Отображаемое имя (например, "Gentle Default")
// - summary: Краткий слоган
// - description: Подробное описание
// - purpose: Когда использовать этот пресет
// - systemImageString: Имя SF Symbol для UI
// - spec: Сам GentleDesignSystemSpec
Пресет Описание Лучше всего для
Gentle Default Спокойная, сбалансированная основа Универсальная отправная точка с чёткой иерархией
Classic Tan Тёплый, вневременной с земляными тонами Приложения, выигрывающие от теплоты и традиций
Modern Gray Элегантный, минималистичный с нейтральной основой Бизнес-приложения, где важна ясность
Soft Green Свежий, природный с успокаивающими акцентами Здоровье, продуктивность, спокойный фокус
Editorial Paper Изысканный, вдохновлённый печатью Контентоёмкие приложения, длинные тексты
Technical Blue Точный, надёжный с синими акцентами Инструменты разработчика, дашборды
Bold Orange Яркий, энергичный с сильным присутствием Приложения, мотивирующие к действию
Elegant Purple Утончённый, роскошный с насыщенными тонами Лайфстайл, творчество, премиум-приложения
Compact Mint Плотный, эффективный с мятными акцентами Интерфейсы с большим объёмом данных

Использование пресетов

// Применить пресет к менеджеру тем
@GentleThemeManagerRuntime private var manager

// Найти и применить пресет
if let editorialPreset = GentleDesignSystemSpec.allPresets.first(where: { $0.name == "Editorial Paper" }) {
    manager.theme.editableSpec = editorialPreset.spec
}

Создание выбора тем

Демо-приложение включает ThemePickerView, отображающий все пресеты как интерактивные карточки. Каждая карточка показывает предпросмотр типографики и цветов пресета, используя собственную тему пресета:

Создание выбора тем
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) {
          // Содержимое карточки отрисовывается с собственным стилем пресета
          ThemePresetCard(preset: preset)
      }
  }
}

Доступные токены

Типографические роли
17 семантических текстовых ролей, организованных по размерной шкале (xxl > xl > l > ml > m > ms > s):
Шкала Роли
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

Каждая роль разрешается в GentleTypographyRoleSpec, содержащий: pointSize, weight, design, width, relativeTo, lineSpacing, letterSpacing, isUppercased и colorRole.

Роли кнопок и анимации

Роли кнопок

primary · secondary · tertiary · quaternary · destructive

Анимации кнопок

Анимация Описание
unknown Без анимации
subtlePress Тонкая обратная связь при нажатии
squish Эффект сжатия при нажатии
pop Эффект выскакивания
bouncy Пружинная анимация
springBack Сжимается при нажатии, отскакивает за пределы исходного размера, затем стабилизируется
Роли поверхностей `appBackground` · `card` · `cardElevated` · `cardSecondary` · `chrome` · `overlaySheet` · `overlayPopover` · `overlayScrim` · `floatingPanel` · `floatingWidget`
Цветовые роли
Категория Роли
Текст (9) textPrimary, textSecondary, textTertiary, textOnPrimaryCTA, textOnDestructive, textOnOverlay, textOnOverlaySecondary, textOnScrim, textOnScrimSecondary
Поверхности (6) background, surfaceBase, surfaceCardSecondary, surfaceTint, surfaceScrim, borderSubtle
Действия (2) primaryCTA, destructive
Тема (2) themePrimary, themeSecondary

Используйте семантические группировки: GentleColorRole.textRoles, .surfaceRoles, .actionRoles, .themeRoles Используйте проверки принадлежности: role.isTextRole, .isSurfaceRole, .isActionRole, .isThemeRole

Токены отступов и скруглений

Токены отступов xs (4) · s (8) · m (12) · l (16) · xl (24) · xxl (32)

Токены скруглений small (8) · medium (12) · large (20) · pill (999)


Требования

  • iOS 18.0+
  • Swift 6.1+

Примечание об инструментах

Часть черновой работы и редакторской доработки в этом репозитории была ускорена с помощью больших языковых моделей (включая ChatGPT, Claude и Gemini) под непосредственным руководством, проверкой и окончательным утверждением человека. Все технические решения, код и архитектурные выводы авторизованы и проверены сопровождающим репозитория.

Посетители