# BaoLife iOS App — Best Practices Analysis

**Date:** 2026-02-26
**Status:** Phases 1, 2, 4, 5 complete — Phase 3 (Architecture split) pending

### Completed Work

**Phase 1 (Quick Wins) — DONE:**
- [x] 8 `try!` crash sites replaced with safe decoding
- [x] 42 `print()` calls gated behind `#if DEBUG`
- [x] 19 no-op @Published guards added to "u" handler
- [x] `personRecieved` → `personReceived` spelling fix (5 files)
- [x] Speed constants consolidated to `Constants.GameSpeed.allLevels`
- [x] Animation duplicates consolidated (CozyAnimations delegates to AppAnimations)
- [x] ChatView extracted: new `ChatMessagesList.swift` component (935→835 lines)

**Phase 4 (Performance) — DONE:**
- [x] 4.3: ChatView O(n*m) dedup → O(1) Set-based lookups
- [x] 4.4: LifeTimelineCard `.id(selectedFilter)` for stable sort identity
- [x] 4.5: SoundManager preloads 12 common sounds at launch
- [x] 4.7: UIScreen.main.bounds replaced with GeometryReader (13 usages, 7 files)

**Phase 5 (Code Quality) — PARTIAL:**
- [x] DateIdea class → struct with CodingKeys (snake_case → camelCase)
- [x] FocusOption `focus_name` → `focusName` with CodingKeys
- [x] Global `wsEnvironment` → `WebSocketEnvironment.current` static property

**Phase 2 (@Observable Migration) — DONE:**
- [x] iOS deployment target bumped to 17.0
- [x] 12 ObservableObject classes → @Observable final class (WebSocketService, Player, Person, Activity, GameStateViewModel, PlayerViewModel, ToastManager, SoundManager, TooltipManager, AnalyticsManager, DeepLinkManager, DateMiniGameState)
- [x] All @Published property wrappers removed
- [x] All @EnvironmentObject → @Environment(Type.self) across 70+ files
- [x] All @ObservedObject → plain var (11 files)
- [x] All @StateObject → @State (except StoreManager which stays ObservableObject/NSObject)
- [x] @Bindable added where $ bindings needed (ContentView, MoreView)
- [x] EmptyStateView shared component created
- [x] Corner radii standardized to AppSpacing variants (30+ sites)
- [x] Xcode build verified: 0 errors

**Remaining (Phase 3 and rest of 5):**
- [ ] WebSocketService split into focused services
- [ ] Singleton/EnvironmentObject dual-access consolidation
- [ ] Haptic calls → route through HapticFeedback.swift (90 sites)
- [ ] DispatchQueue.main.asyncAfter → Task/onChange (44 files)

## Executive Summary

The app is **well-structured** with a clean feature-driven architecture, 174 Swift files, and a solid design system. However, three systemic issues dominate: the **WebSocketService god object** (1,406 lines, 38 @Published properties), **cascading view redraws** from nested ObservableObjects, and **crash-prone JSON parsing** with `try!` calls. Fixing these will dramatically improve performance, stability, and maintainability.

---

## CRITICAL — Fix These First

### 1. Replace `try!` with proper error handling (crash sites)

**File:** `WebSocketService.swift`

Any malformed server data will crash the app. These `try!` calls decode arrays of `AnswerOption`, `ExtracurricularClass`, `FocusOption`, `StoreItem`, `InAppPurchaseItem`, `OccupationClass`, and `Habit`.

**Fix:** Replace with `try?` or `do/catch`:
```swift
// Before (crashes on bad data):
self.extracurriculars = try! decoder.decode([ExtracurricularClass].self, from: jsonData ?? Data())

// After:
if let data = jsonData {
    self.extracurriculars = (try? decoder.decode([ExtracurricularClass].self, from: data)) ?? []
}
```

### 2. Cascading view redraws from 38 @Published properties

Every `"u"` tick message mutates up to **15 @Published properties individually**, each firing `objectWillChange.send()`. With 61 files consuming `@EnvironmentObject var webSocketService`, this triggers body re-evaluation across the entire view tree — potentially dozens of times per second at fast game speeds.

**Fix (immediate):** Guard against no-op updates:
```swift
// Before:
self.person.money = money

// After:
if self.person.money != money { self.person.money = money }
```

**Fix (medium-term):** Migrate to `@Observable` macro (see Phase 2).

### 3. Spelling error propagated across multiple files

`personRecieved` (should be `personReceived`) in WebSocketService.swift, ParsingHelpers.swift, PlayerViewModel.swift, and NPCProfileView.swift. This will confuse every future developer.

---

## Phase 1: Quick Wins (Current Sprint)

### 1.1 — Fix `try!` crash sites in WebSocketService.swift
- Find all `try!` in the file
- Replace with `do/catch` or `try?` with sensible defaults
- Log decode failures for debugging

### 1.2 — Fix spelling: `personRecieved` → `personReceived`
- WebSocketService.swift
- ParsingHelpers.swift
- PlayerViewModel.swift
- NPCProfileView.swift
- Any other files with the typo

### 1.3 — Gate `print()` behind `#if DEBUG`
- 42+ print statements in WebSocketService.swift including raw message payloads
- Wrap in `#if DEBUG` blocks
- Prevents sensitive data logging in production

### 1.4 — Guard against no-op @Published updates
- In WebSocketService.swift `readMessage()` handler for `"u"` messages
- Add value-change checks before assigning @Published properties
- Reduces unnecessary SwiftUI view invalidation

### 1.5 — Consolidate duplicate animation constants
- `AnimationUtilities.swift` and `CozyAnimations.swift` have overlapping definitions
- Merge into single source of truth
- Update all references

### 1.6 — Consolidate speed level constants
- Speed levels `[5000, 1000, 500, 50, 20, 1]` defined in ChatView AND GameControlsCard
- Move to Constants.swift
- Update both references

### 1.7 — Extract ChatView subviews (934 lines → ~200 each)
- Extract message list into `ChatMessagesList`
- Extract input bar into `ChatInputBar`
- Extract character header into `ChatCharacterHeader`
- Extract typing indicators into `ChatTypingIndicator`

---

## Phase 2: @Observable Migration (Future)

| Step | What | Effort |
|------|------|--------|
| 2.1 | Bump deployment target to iOS 17 | Low |
| 2.2 | Migrate simple managers first (ToastManager, SoundManager, TooltipManager) | Low |
| 2.3 | Convert Player and Person from classes to structs | Medium |
| 2.4 | Migrate WebSocketService to @Observable | High |
| 2.5 | Update all 61 consuming views | Medium (mechanical) |
| 2.6 | Simplify/eliminate GameStateViewModel | Medium |

## Phase 3: Architecture Improvements (Future)

### Split WebSocketService into focused services
| Service | Responsibility |
|---------|---------------|
| `WebSocketConnection` | Transport: connect, send, receive, retry |
| `MessageRouter` | Parse JSON, dispatch to domain handlers |
| `GameStateStore` | Player/person state, time, season |
| `EventService` | Questions, life events, claiming |
| `ConversationService` | Active conversations, messages |
| `RetentionService` | Achievements, daily rewards, quests |
| `MonetizationService` | Energy refills, time skips, IAP |

## Phase 4: Performance Fixes (Future)

| # | Issue | Fix |
|---|-------|-----|
| 4.1 | 50+ `repeatForever` animations | Gate on device capability |
| 4.2 | Double JSON serialization | Define typed Codable envelope |
| 4.3 | ChatView O(n*m) dedup | Use Set<String> for O(1) lookup |
| 4.4 | LifeTimelineCard re-sorts on every render | Cache sorted result |
| 4.5 | 13 of 19 sounds not preloaded | Preload at launch |
| 4.6 | 82 DispatchQueue.main.asyncAfter calls | Replace with Task/onChange |
| 4.7 | UIScreen.main.bounds deprecated (13 usages) | Use GeometryReader |

## Phase 5: Code Quality (Future)

| Issue | Fix |
|-------|-----|
| snake_case in Swift models | Add CodingKeys enum |
| DateIdea is class, should be struct | Change to struct |
| Singleton/EnvironmentObject dual-access | Pick one pattern |
| 30+ hardcoded corner radii | Use AppSpacing variants |
| 15 inline empty state views | Extract shared EmptyStateView |
| 90 haptic calls bypass utility | Route through HapticFeedback.swift |
| Mixed notification mechanisms | Standardize on @Published/Combine |
| Global variable wsEnvironment | Make static property |
