MadLibs
A SwiftUI word game app for iOS 17+ with two distinct play modes: Classic (fill-in-the-blank) and Creative (word-bank-driven story generation). Ships with 30 story templates across 3 categories, a custom template editor, word bank management, biometric + PIN authentication, and 6 user-customizable themes with ambient animations. ~5,881 LOC Swift. Zero third-party dependencies.
Overview
MadLibs is the word game genre brought to mobile as a pure-Swift, offline-first iOS app. Players choose between Classic Mode — where they fill in blanks one at a time before revealing the finished story — and Creative Mode, where a user-built word bank drives dynamic story generation without any AI or network calls.
The app graduated from v1.0 (core gameplay) through v1.4 (theme system, template editor, haptics, biometric auth), landing in maintenance mode at v1.4. v1.5 (Classic/Creative UX refinement) is planned but not started.
GitHub: irishrocker1125/MadLibs
Tech Stack
| Technology | Version | Purpose |
|---|---|---|
| Swift | 5.x | Primary language |
| SwiftUI | iOS 17+ | Declarative UI (@Observable, NavigationStack) |
| Xcode | Latest | Build system / IDE |
| LocalAuthentication | Built-in | Face ID / Touch ID |
| Security (Keychain) | Built-in | Secure PIN storage via KeychainService |
| UserDefaults | Built-in | Persistence — stories, word banks, templates, settings |
| XCTest | Built-in | Unit tests (MadLibsTests target, ~50-80% coverage) |
| Foundation | Built-in | Core types, JSON encoding/decoding |
No third-party packages. Pure Apple frameworks.
Architecture
Feature-based module organization with shared Components/ and Core/ layers.
MadLibs/MadLibs/
MadLibsApp.swift # App entry point
ContentView.swift # Root view
RootView.swift # Navigation root
Models/
WordType.swift # 11 word types with examples
StoryTemplate.swift # Template + StorySegment DSL
WordBank.swift # Word collection with smart selection
Core/
Theme/
AppTheme.swift # Customizable color palette (6 themes)
ThemeManager.swift # @Observable theme state
Authentication/
AuthenticationManager.swift # Biometric + PIN auth
KeychainService.swift # Keychain credential storage
Components/
ScaleButtonStyle.swift # 0.97-scale press feedback
Buttons/MesmerizingButtonStyle # Animated gradient button
Cards/MesmerizingCard # Ambient card container
Inputs/MesmerizingTextField # Styled text input
Effects/AmbientBackground # Animated gradient background
Data/
SampleStories.swift # 30 templates (10 everyday/adventure/fantasy)
Extensions/
Color+Hex.swift # Hex-to-SwiftUI Color
Animation+Presets.swift # Standardized animation curves
MadLibs/MadLibsTests/ # XCTest unit tests
State management: @Observable macro (iOS 17) on service classes (AuthenticationManager, ThemeManager). No Combine, no ObservableObject.
Navigation: NavigationStack + typed Route enum for type-safe routing.
Template DSL: StorySegment enum (.text / .blank) represents templates as alternating text and fill-in positions.
Key Features
- Classic Mode — Fill blanks sequentially, reveal completed story
- Creative Mode — Word bank drives story generation; auto-generates when only one template matches
- Custom Template Editor — Create and store user-defined templates; blank insertion uses “insert after focused segment” workaround (SwiftUI TextField doesn’t expose cursor position)
- Word Bank — Single cumulative collection, auto-saved on mutation; 11
WordTypecategories (noun, verb, adjective, etc.) - 30 Built-in Templates — 10 everyday, 10 adventure, 10 fantasy; merged with custom templates via
allTemplatescomputed property - Theme System — 6 themes with hue selection;
CodableColorwrapper bridgesSwiftUI.ColortoCodableforUserDefaultspersistence - Biometric + PIN Auth — Face ID / Touch ID via
LocalAuthenticationwith PIN fallback stored in Keychain - Haptics — Medium impact for primary actions, light for secondary
Key Decisions
| Decision | Rationale | Outcome |
|---|---|---|
| Two separate play modes | Distinct user mental models: fill-in vs. generative | Good |
| SwiftUI only, iOS 17.0 target | @Observable, NavigationStack — clean modern architecture | Good |
| State-driven view switching | Simpler than deep navigation for contained gameplay flow | Good |
[WordType: [String]] word bank | Multiple words per type enables creative generation | Good |
generateStory static on WordBank | Keeps generation logic near word data | Good |
UserDefaults for persistence | Sufficient for data sizes; no CoreData setup needed | Good |
| Single word bank per user | Simpler mental model than multiple named banks | Good |
| Auto-save on word mutation | No explicit save button; prevents data loss | Good |
allTemplates combined source | Merges built-in + custom in single computed property | Good |
| Medium detent sheet for blank editing | Inline editing feel without full-screen modal | Good |
CodableColor wrapper | Bridges SwiftUI.Color to Codable for theme persistence | Good |
| Biometric + PIN dual auth | Face ID/Touch ID with PIN fallback for full coverage | Good |
Patterns Established
| Pattern | Description | Reusable |
|---|---|---|
ScaleButtonStyle | 0.97-scale press feedback, used in 4+ views | Yes |
MesmerizingButtonStyle | Animated gradient button with ambient glow | Yes |
MesmerizingCard | Card with ambient gradient background | Yes |
AmbientBackground | Animated gradient atmospheric layer | Yes |
AppTheme + ThemeManager | User-customizable theme with hue selection | Yes |
@Environment injection | Services injected via .environment() — clean DI | Yes |
NavigationStack + Route enum | Type-safe routing with Hashable enum cases | Yes |
StorySegment DSL | Alternating .text / .blank template representation | Yes |
| Observable persistence service | @Observable CRUD with UserDefaults auto-sync | Yes |
Technical Debt
| Item | Priority | Notes |
|---|---|---|
| Classic Mode input flow | Medium | Keyboard behavior and field interactions not yet polished |
| Creative Mode word collection UX | Medium | Template matching clarity needs improvement |
| Template Editor blank positioning | Medium | Insert position not always intuitive |
| No iCloud sync | Low | Data is device-local only |
| No sharing features | Low | Explicitly out of scope |
Lessons Learned
@Observable(iOS 17) eliminatesObservableObject+@Publishedboilerplate with less ceremony- SwiftUI
TextFielddoesn’t expose cursor position — design insert UX around segment focus instead - Template generation without AI: 30 templates across 3 categories provides solid variety; start with ~10 per category
- Auto-generate when only 1 template matches — skip selection screens when the result is predetermined
- Biometric lockout requires PIN fallback — always provide a secondary auth method
gray-matter-style:CodableColorshows the pattern for bridging non-Codable platform types toUserDefaults
Related
- Observable State Patterns
- Swift Conventions
- DiceRoller — sibling iOS app sharing
Color+Hexextension