Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9b85642
Migrate UIKit navigation to pure SwiftUI NavigationStack
g-enius Feb 25, 2026
a2162a4
Fix navigation bar title display to match UIKit version
g-enius Feb 25, 2026
4c39114
Match UIKit nav bar styles: inline titles, fix detail toolbar order
g-enius Feb 25, 2026
f329077
Match Profile dismiss button style to UIKit version
g-enius Feb 25, 2026
8451205
Teal app icon for SwiftUI branch, fix cancellation handling
g-enius Feb 25, 2026
1372167
Fix stale references, update featured items, large nav titles
g-enius Feb 25, 2026
d09b3e9
Add concurrency patterns featured item
g-enius Feb 25, 2026
cb88cfd
Remove navigation title from detail screen
g-enius Feb 26, 2026
b5f7846
Remove stale UIKit coordinator files from rebase
g-enius Feb 26, 2026
9ca857a
Use polling instead of fixed sleep in search tests
g-enius Feb 26, 2026
f9a4008
Revert debounce scheduler to RunLoop.main and simplify search tests
g-enius Feb 26, 2026
64bc011
Adapt config for navigation-stack branch
g-enius Feb 27, 2026
4ae5230
Move @Service properties to class level
g-enius Feb 27, 2026
1217d47
Remove unnecessary Task.sleep from pending deep link execution
g-enius Feb 27, 2026
b7ae07a
Extract named navigation methods on AppCoordinator
g-enius Feb 28, 2026
4e0cfc7
Update docs to reflect named navigation methods
g-enius Feb 28, 2026
1f7f6bb
Add anti-pattern rule for inline navigation property manipulation
g-enius Feb 28, 2026
b218e00
Split TechnologyDescriptions to fix type_body_length warning
g-enius Feb 28, 2026
a9b182f
Register toast service in LoginSession and observe via serviceDidRegi…
g-enius Feb 28, 2026
0e19e78
Move routing table to AppCoordinator.destinationView(for:)
g-enius Feb 28, 2026
ca76237
Add @ViewBuilder to destinationView for future routing
g-enius Feb 28, 2026
87fba07
Document why AppRootView and MainTabView live in Coordinator
g-enius Feb 28, 2026
fbc6063
Add comment about chaining navigationDestination for more types
g-enius Feb 28, 2026
e8cfce2
Document ownership wrapper pattern and add anti-pattern rule
g-enius Feb 28, 2026
dcd6f14
Add routing table example comment to destinationView
g-enius Feb 28, 2026
a20b7b7
Replace Task.sleep(nanoseconds:) with Duration API
g-enius Mar 1, 2026
1bbf003
Fix stale docs: remove onPop/onShare, add toast to LoginSession, @tes…
g-enius Mar 1, 2026
c4134c6
Fix UI module description — remove UIKit reference
g-enius Mar 2, 2026
dcc291b
Fix stale async-sequence reference in TechnologyDescriptions
g-enius Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 9 additions & 19 deletions .claude/agents/change-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ Review all recent code changes thoroughly and provide a structured, actionable a

## Project Context

- **Architecture**: MVVM-C with Combine (main branch), NavigationStack + Combine (navigation-stack branch), AsyncSequence + @Observable (observation branch)
- **Branch**: feature/navigation-stack — Pure SwiftUI, NavigationPath, single AppCoordinator (ObservableObject), Combine
- **Packages**: `FunCore` → `FunModel` → `FunViewModel` / `FunServices` → `FunUI` → `FunCoordinator`
- **Dependency direction**: Never import upward. ViewModel must NOT import UI or Coordinator.
- **UIKit**: Zero UIKit in this branch — flag any `import UIKit` as a critical issue
- **DI**: ServiceLocator with `@Service` property wrapper, session-scoped (LoginSession / AuthenticatedSession)
- **Testing**: Swift Testing framework, mocks in FunModelTestSupport
- **Lint**: SwiftLint with custom rules (no_print, weak_coordinator_in_viewmodel, no_direct_userdefaults)
Expand All @@ -33,26 +34,23 @@ Review all recent code changes thoroughly and provide a structured, actionable a
- **Similar patterns elsewhere**: Search the codebase for code following the same pattern. If the same improvement applies elsewhere, flag each location.
- **Consistency**: Do changes follow existing patterns?
- **No orphaned references**: Stale imports, unused variables, dead code paths?
- **Edge cases**: Boundary conditions, nil/optional handling, error paths?

### Step 3: Architecture Check
- Package dependency direction respected?
- No `import UIKit` in ViewModel or Model
- No `import UIKit` — pure SwiftUI branch
- No coordinator references in ViewModels (except weak closures)
- No `print()` — use LoggerService
- No `UserDefaults.standard` outside Services
- Navigation logic only in Coordinators
- Navigation logic only in Coordinators (AppCoordinator)
- NavigationPath mutations only in coordinator, not in Views
- Protocols in Core (reusable) or Model (domain), never in Services/ViewModel/UI/Coordinator
- Detect which branch you're on and enforce the right reactive pattern:
- `main`: Combine + UIKit coordinators
- `feature/navigation-stack`: Combine + NavigationPath + ObservableObject
- `feature/observation`: AsyncSequence + StreamBroadcaster + @Observable, zero Combine
- Reactive pattern: Combine (`@Published`, `@StateObject`, `@ObservedObject`, `.sink`)

### Step 4: Correctness Check
- **Logic errors**: Algorithms, conditions, control flow
- **Type safety**: Force unwraps, force casts, unsafe assumptions
- **Concurrency**: `@MainActor` isolation, `Sendable` conformance, thread safety (Swift 6 strict)
- **Memory management**: `[weak self]` in closures, no retain cycles. `self?.` preferred over `guard let self` for async ViewModel work.
- **Concurrency**: `@MainActor` isolation, `Sendable` conformance, Swift 6 strict
- **Memory management**: `[weak self]` and `[weak coordinator]` in closures
- **API contracts**: Public interfaces used correctly

### Step 5: Quality Check
Expand Down Expand Up @@ -91,15 +89,7 @@ Ship it | Minor fixes needed | Needs significant work
1. **Be calibrated**: This is a demo/portfolio app. Don't demand enterprise patterns.
2. **Be specific**: Reference exact files and lines. No vague feedback.
3. **Be actionable**: Every finding must include a concrete recommendation.
4. **Don't over-engineer**: If the codebase uses a pattern (e.g., `fatalError` for service resolution), don't flag it.
4. **Don't over-engineer**: If the codebase uses a pattern, don't flag it.
5. **Focus on the diff**: Review what changed, not pre-existing code.
6. **Verify before flagging**: Read actual code before claiming something is missing.
7. **Count honestly**: Fewer than 3 issues? That's fine. Don't inflate.

## Self-Verification

Before delivering your review:
- Re-read each finding: "Is this actually a problem, or am I being overly cautious?"
- "Did I miss any changed files?"
- "Are my recommendations correct and compatible with the codebase?"
- "Would I stand behind each finding in a review discussion?"
2 changes: 1 addition & 1 deletion .claude/skills/cross-platform/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ args: "<feature-name>"
Compare the implementation of a feature across Fun-iOS and Fun-Android to find unintentional divergences.

## Project Paths
- **iOS**: `~/Documents/Source/Fun-iOS/`
- **iOS**: `~/Documents/Source/Fun-iOS-NavigationStack/`
- **Android**: `~/Documents/Source/Fun-Android/`

## Steps
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/pull-request/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Create a draft PR following the team's quality standards.
- `git diff main...HEAD` to review all changes
- Verify package dependency direction isn't violated
- Check for any `print()`, `UserDefaults.standard`, or other anti-patterns
- Verify zero UIKit imports (this branch is pure SwiftUI)

3. **Accessibility checklist** (for UI changes)
- Dynamic Type: Do text elements scale with user font size preference?
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ Review all recent code changes for completeness, correctness, and consistency wi

3. **Architecture check**
- Verify package dependency direction: `Coordinator → UI → ViewModel → Model → Core`, `Services → Model → Core`
- No `import UIKit` in ViewModel or Model
- No `import UIKit` anywhere — this branch is pure SwiftUI
- No coordinator references in ViewModels (except weak closures)
- No `print()` — use LoggerService
- No `UserDefaults.standard` outside Services
- Navigation logic only in Coordinators
- Protocols in Core (reusable) or Model (domain), never in Services/ViewModel/UI/Coordinator
- Branch-specific: Combine patterns (this branch uses Combine + UIKit coordinators)
- Branch-specific: Combine + NavigationPath + single AppCoordinator (ObservableObject)

4. **Similar pattern search**
- Search the codebase for code that follows the same pattern as what changed
- If the same improvement should be applied elsewhere, flag each location

5. **Correctness check**
- Logic errors, type safety, concurrency (Swift 6 strict), memory management (`[weak self]`)
- Logic errors, type safety, concurrency (Swift 6 strict), memory management (`[weak self]`, `[weak coordinator]`)
- Verify `@MainActor` isolation, `Sendable` conformance where needed

6. **Cross-platform parity**
Expand Down
29 changes: 18 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,27 @@ Coordinator → UI → ViewModel → Model → Core
Never import upward. ViewModel must NOT import UI or Coordinator. Model must NOT import Services.

## Anti-Patterns (Red Flags)
- `import UIKit` in ViewModel or Model packages — UIKit belongs in UI and Coordinator only
- `import UIKit` anywhere — this branch is pure SwiftUI, zero UIKit
- Coordinator references in ViewModels (except weak optional closures) — retain cycle risk
- `print()` anywhere — use LoggerService
- `UserDefaults.standard` outside Services — use FeatureToggleService
- Adding `fatalError()` for missing services — ServiceLocator.resolve() already crashes with `fatalError` if a service isn't registered; don't add redundant guards
- Navigation logic in Views — navigation decisions belong in Coordinators only
- Navigation logic in Views — all navigation (push, pop, tab switch, modal present/dismiss) must go through named AppCoordinator methods (`showDetail`, `selectTab`, `showProfile`, etc.), never inline property manipulation like `coordinator.homePath.append(item)` or `coordinator.isProfilePresented = true`
- Protocol definitions in Services — domain protocols go in Model, reusable abstractions in Core
- Wrong ownership annotations — tab content wrappers must use `@StateObject` to own ViewModels (not `@ObservedObject`). `@ObservedObject` on a ViewModel means it gets recreated on every re-render. Conversely, the coordinator must be `let` or `@ObservedObject` (not `@StateObject`) since the wrapper doesn't own it.

## Architecture (this branch: main)
- **Entry point**: UIKit `AppDelegate` + `SceneDelegate` (scene-based lifecycle)
- **Navigation**: 6 UIKit coordinators — `AppCoordinator`, `BaseCoordinator`, `LoginCoordinator`, `HomeCoordinator`, `ItemsCoordinator`, `SettingsCoordinator`
- **Views**: SwiftUI views embedded in UIHostingController via UIViewControllers
- **Reactive**: Combine (`@Published`, `CurrentValueSubject`, `.sink`)
- **ViewModel → Coordinator**: Optional closures (`onShowDetail`, `onShowProfile`, etc.)
## Architecture (this branch: feature/navigation-stack)
- **Entry point**: SwiftUI `@main App` struct (`FunApp.swift`) — no AppDelegate or SceneDelegate
- **Navigation**: Single `AppCoordinator: ObservableObject` with per-tab `NavigationPath`
- **Views**: Pure SwiftUI views, no UIHostingController or UIViewControllers
- **Reactive**: Combine (`@Published`, `@StateObject`, `@ObservedObject`, `.sink`)
- **ViewModel → Coordinator**: Optional closures wired in tab content wrappers via `.task { viewModel.onShowDetail = { ... } }`
- **Tab bar**: SwiftUI `TabView(selection: $coordinator.selectedTab)`
- **Push nav**: `coordinator.showDetail(item, in: .home)` — named methods on AppCoordinator
- **Modals**: `.sheet(isPresented: $coordinator.isProfilePresented)`
- **DI**: ServiceLocator with `@Service` property wrapper, session-scoped (LoginSession / AuthenticatedSession)
- **Coordinator-owned views**: `AppRootView`, `MainTabView`, and tab content wrappers live in `Coordinator` (not `FunUI`) because they depend on `AppCoordinator`. Moving them to `FunUI` would create a circular dependency (`Coordinator → UI → Coordinator`). Pure reusable views (`HomeView`, `DetailView`, etc.) stay in `FunUI`.
- **Ownership wrappers**: Tab content wrappers (`HomeTabContent`, `ItemsTabContent`, etc.) use `@StateObject` to **own** their ViewModel and `@ObservedObject` (or `let`) for the coordinator passed from the parent. `@StateObject` ensures the ViewModel survives re-renders; `@ObservedObject` means the wrapper doesn't own the coordinator. Pure views in `FunUI` take `@ObservedObject var viewModel` since the wrapper owns it.

## Rule Index
Consult these files for detailed guidance (not auto-loaded — read on demand):
Expand All @@ -69,11 +75,12 @@ Consult these files for detailed guidance (not auto-loaded — read on demand):

## Code Style
- Swift 6 strict concurrency, iOS 17+
- SwiftUI + UIKit hybrid, MVVM-C with Combine
- ViewModels use closures for navigation (no coordinator protocols)
- Pure SwiftUI (NavigationStack), MVVM-C with Combine
- Single AppCoordinator: ObservableObject with @Published NavigationPath per tab
- ViewModels use closures for navigation, wired in tab content wrappers
- Navigation logic ONLY in Coordinators, never in Views
- Protocol placement: Core = reusable abstractions, Model = domain-specific
- ServiceLocator with @Service property wrapper (assertionFailure, not fatalError)
- ServiceLocator with @Service property wrapper
- Combine over NotificationCenter for reactive state

## Testing
Expand Down
4 changes: 2 additions & 2 deletions Coordinator/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import PackageDescription
let package = Package(
name: "Coordinator",
platforms: [
.iOS(.v15),
.macCatalyst(.v15),
.iOS(.v16),
.macCatalyst(.v16),
],
products: [
.library(name: "FunCoordinator", targets: ["FunCoordinator"]),
Expand Down
Loading