FortuneTeller
A mystical fortune-telling application for iOS built with SwiftUI. Features multiple prediction modes including Eight Ball, Daily Fortune, Fortune Cookie, and Tarot-inspired readings, all powered by an offline combinatorial generation engine.
Key Decisions
| Decision | Rationale | Outcome |
|---|---|---|
| MVVM architecture with @MainActor ViewModels | Clean separation of UI and logic, thread-safe | 4 ViewModels drive 4 modes cleanly |
| PredictionProvider protocol | Abstraction enables swappable prediction engines | AlgorithmicProvider implements current logic |
| Offline combinatorial word-pool prediction | Offline-first, instant results, zero latency | Thousands of unique combinations generated |
| 17% humor injection chance | Surprise delight factor in fortunes | Users occasionally get humor results |
| ThemeManager as @EnvironmentObject | Centralized, reactive theming across all views | Theme changes propagate instantly |
| UserDefaults for all persistence | Simple, no CoreData overhead for small data sets | Theme, streaks, history all persisted |
| Lazy-loaded haptic generators | No startup overhead, generators created on first use | Snappy performance |
| AudioServicesPlaySystemSound for SFX | Lightweight, no AVAudioPlayer complexity needed | Minimal code, reliable system sounds |
| CAEmitterLayer via UIViewRepresentable | Hardware-accelerated particles for mystical effects | Smooth sparkle and mystical visuals |
| Calendar date methods for streak logic | Reliable day comparison vs simple DateComponents | Correct isDateInToday/Yesterday detection |
| Init parameter DI for HistoryManager | ViewModels can’t use @EnvironmentObject, need testable DI | Testable, optional parameter with fallback |
| 100-item history limit | Prevents unbounded UserDefaults growth | Automatic trimming on save |
| Locked themes visible at opacity(0.6) | Clear visual indication of lock status | Motivates users to unlock new themes |
Patterns Established
| Pattern | Description | Reusable |
|---|---|---|
| MVVM with @MainActor ViewModels | One ViewModel per fortune mode | Yes |
| Protocol-based service abstraction | PredictionProvider protocol with AlgorithmicProvider impl | Yes |
| Word pool combinatorial generation | Subjects + verbs + outcomes with varied sentence structures | Yes |
| ThemeManager @EnvironmentObject | App-wide theming via single observable manager | Yes |
| Singleton streak tracking | Daily visit tracking with recordVisit() logic | Yes |
| History with deduplication and limit | HistoryManager saves predictions, caps at 100 items | Yes |
| Achievement system unlock | AchievementManager checks streaks and usage milestones | Yes |
| ParticleEmitterView wrapper | CAEmitterLayer wrapped for SwiftUI with 3 particle styles | Yes |
| HapticService with named patterns | Semantic haptic API independent of UIKit details | Yes |
Lessons Learned
| Lesson | Context | Impact |
|---|---|---|
| System sounds are lightweight and reliable | Using AudioServicesPlaySystemSound instead of AVAudioPlayer | Reduced audio management overhead |
| Extract all colors to theme system | Hardcoded hex colors in cookie view created maintenance burden | Extract to theme system for consistency |
| Avoid try? for silent JSON encoding | Failing silently can lead to data loss without user notification | Use do/catch with logging at minimum |
| Timezone-aware daily fortune keys | Timezone changes could cause duplicate daily fortunes | Use Calendar.startOfDay for consistent date key generation |
| CAEmitterLayer outperforms Canvas | Hardware-accelerated particles vs SwiftUI Canvas rendering | Handles 30-40 particles smoothly on device |
Technical Debt
| Item | Priority | Notes |
|---|---|---|
| Empty test suite | High | No automated tests; AlgorithmicProvider is a priority target |
| Silent JSON encoding errors | Medium | DailyFortuneViewModel persistence uses try? with no handling |
| Hardcoded cookie colors | Low | Colors in FortuneCookieView.swift should be in theme |
| Undocumented magic numbers | Low | humorChance = 0.17 lacks documented rationale |
| Timezone fragility in keys | Medium | dailyFortune_yyyy-MM-dd could duplicate on shift |
| TestFlight distribution pending | Medium | Phase 4 planned but not executed |