docs: add Dynamic Type implementation design
Replace all custom font sizes with Apple's built-in text styles for accessibility compliance. Remove Theme.FontSize enum entirely. Export files keep fixed sizes for consistent PDF output. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
206
docs/plans/2026-01-11-dynamic-type-design.md
Normal file
206
docs/plans/2026-01-11-dynamic-type-design.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Dynamic Type Implementation Design
|
||||
|
||||
## Overview
|
||||
|
||||
Implement Dynamic Type app-wide by replacing all custom font sizes with Apple's built-in text styles. This ensures accessibility compliance and automatic scaling based on user preferences.
|
||||
|
||||
## Scope
|
||||
|
||||
**What changes:**
|
||||
- Remove `Theme.FontSize` enum entirely
|
||||
- Replace all `.font(.system(size:))` with Apple text styles
|
||||
- Update 20 UI files
|
||||
|
||||
**What stays fixed:**
|
||||
- PDF export files (ProgressCardGenerator, PDFGenerator, MapSnapshotService)
|
||||
- These need predictable dimensions for consistent output
|
||||
|
||||
## Text Style Mapping
|
||||
|
||||
### Apple Text Styles Reference
|
||||
|
||||
| Style | Default Size | Use Case |
|
||||
|-------|--------------|----------|
|
||||
| `.largeTitle` | 34pt | Screen titles, hero text |
|
||||
| `.title` | 28pt | Primary headings |
|
||||
| `.title2` | 22pt | Section headers |
|
||||
| `.title3` | 20pt | Subsection headers |
|
||||
| `.headline` | 17pt semibold | Card titles, emphasis |
|
||||
| `.body` | 17pt | Primary content |
|
||||
| `.callout` | 16pt | Secondary content |
|
||||
| `.subheadline` | 15pt | Supporting text |
|
||||
| `.footnote` | 13pt | Tertiary info |
|
||||
| `.caption` | 12pt | Labels, metadata |
|
||||
| `.caption2` | 11pt | Fine print |
|
||||
|
||||
### Current → Apple Mapping
|
||||
|
||||
| Current Usage | Current Size | Apple Style |
|
||||
|---------------|--------------|-------------|
|
||||
| `Theme.FontSize.heroTitle` | 34 | `.largeTitle` |
|
||||
| `Theme.FontSize.sectionTitle` | 24 | `.title2` |
|
||||
| `Theme.FontSize.cardTitle` | 18 | `.headline` |
|
||||
| `Theme.FontSize.body` | 16 | `.body` |
|
||||
| `Theme.FontSize.caption` | 14 | `.subheadline` |
|
||||
| `Theme.FontSize.micro` | 12 | `.caption` |
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### UI Files (Dynamic Type)
|
||||
|
||||
```
|
||||
SportsTime/Core/Theme/Theme.swift # Remove FontSize enum
|
||||
SportsTime/Core/Theme/ViewModifiers.swift # Update BadgeStyle, SectionHeaderStyle
|
||||
SportsTime/Core/Theme/AnimatedComponents.swift # Update component fonts
|
||||
SportsTime/Features/Home/Views/HomeView.swift
|
||||
SportsTime/Features/Home/Views/SuggestedTripCard.swift
|
||||
SportsTime/Features/Home/Views/LoadingTripsView.swift
|
||||
SportsTime/Features/Trip/Views/TripCreationView.swift
|
||||
SportsTime/Features/Trip/Views/TripDetailView.swift
|
||||
SportsTime/Features/Trip/Views/RegionMapSelector.swift
|
||||
SportsTime/Features/Trip/Views/TimelineItemView.swift
|
||||
SportsTime/Features/Schedule/Views/ScheduleListView.swift
|
||||
SportsTime/Features/Settings/Views/SettingsView.swift
|
||||
SportsTime/Features/Progress/Views/ProgressTabView.swift
|
||||
SportsTime/Features/Progress/Views/ProgressMapView.swift
|
||||
SportsTime/Features/Progress/Views/VisitDetailView.swift
|
||||
SportsTime/Features/Progress/Views/StadiumVisitSheet.swift
|
||||
SportsTime/Features/Progress/Views/AchievementsListView.swift
|
||||
SportsTime/Features/Progress/Views/GameMatchConfirmationView.swift
|
||||
SportsTime/Features/Progress/Views/PhotoImportView.swift
|
||||
SportsTime/SportsTimeApp.swift
|
||||
```
|
||||
|
||||
### Export Files (Keep Fixed)
|
||||
|
||||
```
|
||||
SportsTime/Export/PDFGenerator.swift
|
||||
SportsTime/Export/Services/ProgressCardGenerator.swift
|
||||
SportsTime/Export/Services/MapSnapshotService.swift
|
||||
```
|
||||
|
||||
## Replacement Patterns
|
||||
|
||||
### Before → After Examples
|
||||
|
||||
```swift
|
||||
// Hero titles
|
||||
// Before:
|
||||
.font(.system(size: Theme.FontSize.heroTitle, weight: .bold))
|
||||
// After:
|
||||
.font(.largeTitle)
|
||||
|
||||
// Section headers
|
||||
// Before:
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
// After:
|
||||
.font(.title2)
|
||||
|
||||
// Card titles
|
||||
// Before:
|
||||
.font(.system(size: Theme.FontSize.cardTitle, weight: .semibold))
|
||||
// After:
|
||||
.font(.headline)
|
||||
|
||||
// Body text
|
||||
// Before:
|
||||
.font(.system(size: Theme.FontSize.body))
|
||||
// After:
|
||||
.font(.body)
|
||||
|
||||
// Captions
|
||||
// Before:
|
||||
.font(.system(size: Theme.FontSize.caption))
|
||||
// After:
|
||||
.font(.subheadline)
|
||||
|
||||
// Micro/labels
|
||||
// Before:
|
||||
.font(.system(size: Theme.FontSize.micro, weight: .medium))
|
||||
// After:
|
||||
.font(.caption)
|
||||
```
|
||||
|
||||
### ViewModifiers.swift Updates
|
||||
|
||||
```swift
|
||||
// BadgeStyle
|
||||
// Before:
|
||||
.font(.system(size: Theme.FontSize.micro, weight: .semibold))
|
||||
// After:
|
||||
.font(.caption)
|
||||
|
||||
// SectionHeaderStyle
|
||||
// Before:
|
||||
.font(.system(size: Theme.FontSize.sectionTitle, weight: .bold, design: .rounded))
|
||||
// After:
|
||||
.font(.title2)
|
||||
```
|
||||
|
||||
### Decision Rule for Ambiguous Cases
|
||||
|
||||
When a hardcoded size doesn't map cleanly:
|
||||
- Primary content → `.body`
|
||||
- Secondary/supporting → `.subheadline`
|
||||
- Labels/metadata → `.caption`
|
||||
- Emphasis within body → `.headline`
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Order
|
||||
|
||||
1. **Theme.swift** — Remove `FontSize` enum
|
||||
2. **ViewModifiers.swift** — Update `BadgeStyle` and `SectionHeaderStyle`
|
||||
3. **AnimatedComponents.swift** — Update component fonts
|
||||
4. **Feature views** — Update all 16 view files
|
||||
5. **SportsTimeApp.swift** — Update any root-level fonts
|
||||
6. **Build & fix** — Compiler catches remaining `Theme.FontSize` references
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Dynamic Type in Simulator
|
||||
|
||||
Settings → Accessibility → Display & Text Size → Larger Text
|
||||
|
||||
Or via Xcode scheme arguments:
|
||||
|
||||
```
|
||||
-UIPreferredContentSizeCategoryName UICTContentSizeCategoryAccessibilityExtraExtraExtraLarge
|
||||
```
|
||||
|
||||
### Test Cases
|
||||
|
||||
| Size Category | What to Check |
|
||||
|---------------|---------------|
|
||||
| Default | App looks normal, similar to before |
|
||||
| Large | Text scales up, layouts don't break |
|
||||
| Accessibility XXL | Text very large, no truncation/overlap |
|
||||
| Extra Small | Text scales down, still readable |
|
||||
|
||||
### Key Screens to Test
|
||||
|
||||
- Home (hero title, trip cards)
|
||||
- Trip Creation (form labels, buttons)
|
||||
- Trip Detail (timeline, game info)
|
||||
- Schedule (list items, headers)
|
||||
- Progress (stats, achievements)
|
||||
- Settings (all rows)
|
||||
|
||||
### Potential Layout Issues
|
||||
|
||||
Watch for:
|
||||
- Text truncation in fixed-width containers
|
||||
- Overlapping elements at large sizes
|
||||
- Buttons too small to tap at large sizes
|
||||
|
||||
Fix with:
|
||||
- `.lineLimit(nil)` for multi-line text
|
||||
- `.minimumScaleFactor(0.8)` for tight spaces
|
||||
- `ScrollView` for content that might overflow
|
||||
|
||||
## Future Development Guidelines
|
||||
|
||||
After this migration:
|
||||
- **Always use Apple text styles** (`.body`, `.headline`, `.caption`, etc.)
|
||||
- **Never use `.system(size:)`** in UI code (exception: Export files)
|
||||
- **Test with Dynamic Type** enabled before merging UI changes
|
||||
Reference in New Issue
Block a user