- Implement camera capture and plant identification workflow - Add Core Data persistence for plants, care schedules, and cached API data - Create collection view with grid/list layouts and filtering - Build plant detail views with care information display - Integrate Trefle botanical API for plant care data - Add local image storage for captured plant photos - Implement dependency injection container for testability - Include accessibility support throughout the app Bug fixes in this commit: - Fix Trefle API decoding by removing duplicate CodingKeys - Fix LocalCachedImage to load from correct PlantImages directory - Set dateAdded when saving plants for proper collection sorting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
617 lines
15 KiB
Markdown
617 lines
15 KiB
Markdown
# Architecture Remediation - Executable Task Plan
|
|
|
|
**Project:** PlantGuide iOS App
|
|
**Created:** 2026-01-23
|
|
**Owner:** Senior Developer
|
|
**Source:** ARCHITECTURE_REMEDIATION_PLAN.md
|
|
|
|
---
|
|
|
|
## How to Use This Plan
|
|
|
|
Each task has:
|
|
- **Validation Command**: Run this to verify completion
|
|
- **Done When**: Specific observable criteria
|
|
- **Commit Message**: Use this exact message format
|
|
|
|
Work through tasks in order. Do not skip ahead.
|
|
|
|
---
|
|
|
|
## PHASE 1: Critical Fixes (Crash Prevention)
|
|
|
|
### Task 1.1.1: Add Protocol Conformance Declaration
|
|
|
|
**File:** `PlantGuide/Data/Repositories/InMemoryPlantRepository.swift`
|
|
|
|
**Do:**
|
|
```swift
|
|
// Find this line:
|
|
final class InMemoryPlantRepository { ... }
|
|
|
|
// Change to:
|
|
final class InMemoryPlantRepository: PlantRepositoryProtocol { ... }
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Build the project - should compile without errors
|
|
xcodebuild -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' build 2>&1 | grep -E "(error:|BUILD SUCCEEDED)"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] Build succeeds with no errors
|
|
- [ ] `InMemoryPlantRepository` explicitly declares `PlantRepositoryProtocol` conformance
|
|
|
|
**Commit:** `fix(data): add PlantRepositoryProtocol conformance to InMemoryPlantRepository`
|
|
|
|
---
|
|
|
|
### Task 1.1.2: Implement Missing Protocol Methods (if any)
|
|
|
|
**File:** `PlantGuide/Data/Repositories/InMemoryPlantRepository.swift`
|
|
|
|
**Do:**
|
|
1. Check what methods `PlantRepositoryProtocol` requires
|
|
2. Compare against what `InMemoryPlantRepository` implements
|
|
3. Add any missing methods
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Grep for protocol definition
|
|
grep -A 50 "protocol PlantRepositoryProtocol" PlantGuide/Domain/Interfaces/Repositories/*.swift
|
|
# Compare with implementation
|
|
grep -E "func (save|delete|fetch|get)" PlantGuide/Data/Repositories/InMemoryPlantRepository.swift
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] All protocol-required methods exist in `InMemoryPlantRepository`
|
|
- [ ] Build succeeds
|
|
|
|
**Commit:** `fix(data): implement missing PlantRepositoryProtocol methods in InMemoryPlantRepository`
|
|
|
|
---
|
|
|
|
### Task 1.1.3: Add Protocol Conformance Unit Test
|
|
|
|
**File:** Create `PlantGuideTests/Data/Repositories/InMemoryPlantRepositoryTests.swift`
|
|
|
|
**Do:**
|
|
```swift
|
|
import XCTest
|
|
@testable import PlantGuide
|
|
|
|
final class InMemoryPlantRepositoryTests: XCTestCase {
|
|
func testConformsToPlantRepositoryProtocol() {
|
|
let repo: PlantRepositoryProtocol = InMemoryPlantRepository.shared
|
|
XCTAssertNotNil(repo)
|
|
}
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
xcodebuild test -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' -only-testing:PlantGuideTests/InMemoryPlantRepositoryTests 2>&1 | grep -E "(Test Suite|passed|failed)"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] Test file exists
|
|
- [ ] Test passes
|
|
- [ ] Test explicitly checks protocol conformance at compile time
|
|
|
|
**Commit:** `test(data): add protocol conformance test for InMemoryPlantRepository`
|
|
|
|
---
|
|
|
|
### Task 1.2.1: Identify Current Thread Safety Issue
|
|
|
|
**File:** `PlantGuide/Data/DataSources/Local/CoreData/CoreDataStack.swift`
|
|
|
|
**Do:**
|
|
1. Read the file
|
|
2. Find `lazy var persistentContainer`
|
|
3. Document the race condition scenario
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Check current implementation
|
|
grep -A 10 "lazy var persistentContainer" PlantGuide/Data/DataSources/Local/CoreData/CoreDataStack.swift
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] Understand where race condition occurs
|
|
- [ ] Decision made: Actor / DispatchQueue / Eager init
|
|
|
|
**Commit:** N/A (research task)
|
|
|
|
---
|
|
|
|
### Task 1.2.2: Fix Thread Safety with Eager Initialization
|
|
|
|
**File:** `PlantGuide/Data/DataSources/Local/CoreData/CoreDataStack.swift`
|
|
|
|
**Do:**
|
|
```swift
|
|
// Replace lazy var with let + init
|
|
// BEFORE:
|
|
lazy var persistentContainer: NSPersistentContainer = { ... }()
|
|
|
|
// AFTER:
|
|
let persistentContainer: NSPersistentContainer
|
|
|
|
init() {
|
|
persistentContainer = NSPersistentContainer(name: "PlantGuide")
|
|
persistentContainer.loadPersistentStores { ... }
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Verify no lazy var for persistentContainer
|
|
grep "lazy var persistentContainer" PlantGuide/Data/DataSources/Local/CoreData/CoreDataStack.swift
|
|
# Should return nothing
|
|
|
|
# Build and run tests
|
|
xcodebuild test -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | grep -E "(BUILD|Test Suite)"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] No `lazy var` for `persistentContainer`
|
|
- [ ] Initialization happens in `init()`
|
|
- [ ] All existing Core Data tests pass
|
|
|
|
**Commit:** `fix(coredata): make persistentContainer thread-safe with eager initialization`
|
|
|
|
---
|
|
|
|
### Task 1.3.1: Find All Unowned References in DIContainer
|
|
|
|
**File:** `PlantGuide/Core/DI/DIContainer.swift`
|
|
|
|
**Do:**
|
|
```bash
|
|
grep -n "unowned self" PlantGuide/Core/DI/DIContainer.swift
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
# List all line numbers with unowned self
|
|
grep -n "unowned self" PlantGuide/Core/DI/DIContainer.swift | wc -l
|
|
# Document the count
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] All `unowned self` locations documented
|
|
- [ ] Count recorded: _____ occurrences
|
|
|
|
**Commit:** N/A (audit task)
|
|
|
|
---
|
|
|
|
### Task 1.3.2: Replace Unowned with Weak + Guard
|
|
|
|
**File:** `PlantGuide/Core/DI/DIContainer.swift`
|
|
|
|
**Do:**
|
|
For each occurrence found in 1.3.1:
|
|
```swift
|
|
// BEFORE:
|
|
LazyService { [unowned self] in
|
|
return SomeService(dependency: self.otherService)
|
|
}
|
|
|
|
// AFTER:
|
|
LazyService { [weak self] in
|
|
guard let self else {
|
|
fatalError("DIContainer deallocated unexpectedly")
|
|
}
|
|
return SomeService(dependency: self.otherService)
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Zero unowned self references should remain
|
|
grep "unowned self" PlantGuide/Core/DI/DIContainer.swift
|
|
# Should return nothing
|
|
|
|
# Verify weak self exists
|
|
grep "weak self" PlantGuide/Core/DI/DIContainer.swift | wc -l
|
|
# Should match the count from 1.3.1
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] Zero `unowned self` in DIContainer
|
|
- [ ] All replaced with `weak self` + guard
|
|
- [ ] App launches successfully
|
|
- [ ] Build succeeds
|
|
|
|
**Commit:** `fix(di): replace unowned with weak references in DIContainer`
|
|
|
|
---
|
|
|
|
## PHASE 2: Architectural Consistency
|
|
|
|
### Task 2.1.1: Audit SwiftData Usage
|
|
|
|
**Files:**
|
|
- `PlantGuide/PlantGuideApp.swift`
|
|
- `PlantGuide/Item.swift`
|
|
|
|
**Do:**
|
|
```bash
|
|
# Check for SwiftData imports
|
|
grep -r "import SwiftData" PlantGuide/
|
|
# Check for @Model usage
|
|
grep -r "@Model" PlantGuide/
|
|
# Check for modelContainer usage
|
|
grep -r "modelContainer" PlantGuide/
|
|
```
|
|
|
|
**Validation:**
|
|
Record findings:
|
|
- SwiftData imports: _____ files
|
|
- @Model usages: _____ types
|
|
- ModelContainer usage: _____ locations
|
|
|
|
**Done When:**
|
|
- [ ] Complete inventory of SwiftData usage
|
|
- [ ] Decision documented: Keep SwiftData or Remove
|
|
|
|
**Commit:** N/A (audit task)
|
|
|
|
---
|
|
|
|
### Task 2.1.2: Remove Unused SwiftData (if decision is Core Data only)
|
|
|
|
**Files:**
|
|
- `PlantGuide/PlantGuideApp.swift`
|
|
- `PlantGuide/Item.swift`
|
|
|
|
**Do:**
|
|
1. Delete `Item.swift`
|
|
2. In `PlantGuideApp.swift`:
|
|
- Remove `sharedModelContainer` property
|
|
- Remove `.modelContainer(sharedModelContainer)` modifier
|
|
- Remove `import SwiftData`
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Item.swift should not exist
|
|
ls PlantGuide/Item.swift 2>&1
|
|
# Should return "No such file"
|
|
|
|
# No SwiftData in PlantGuideApp
|
|
grep -E "(SwiftData|modelContainer|sharedModelContainer)" PlantGuide/PlantGuideApp.swift
|
|
# Should return nothing
|
|
|
|
# Build succeeds
|
|
xcodebuild -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' build 2>&1 | grep "BUILD SUCCEEDED"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] `Item.swift` deleted
|
|
- [ ] No SwiftData references in `PlantGuideApp.swift`
|
|
- [ ] Build succeeds
|
|
- [ ] App launches correctly
|
|
|
|
**Commit:** `refactor(app): remove unused SwiftData setup, keeping Core Data`
|
|
|
|
---
|
|
|
|
### Task 2.2.1: Verify CoreDataPlantStorage Conforms to PlantRepositoryProtocol
|
|
|
|
**Do:**
|
|
```bash
|
|
grep -A 5 "class CoreDataPlantStorage" PlantGuide/Data/DataSources/Local/CoreData/
|
|
grep "PlantRepositoryProtocol" PlantGuide/Data/DataSources/Local/CoreData/CoreDataPlantStorage.swift
|
|
```
|
|
|
|
**Validation:**
|
|
- [ ] `CoreDataPlantStorage` explicitly conforms to `PlantRepositoryProtocol`
|
|
|
|
If not conforming, add conformance first.
|
|
|
|
**Done When:**
|
|
- [ ] Conformance verified or added
|
|
|
|
**Commit:** `fix(coredata): ensure CoreDataPlantStorage conforms to PlantRepositoryProtocol`
|
|
|
|
---
|
|
|
|
### Task 2.2.2: Update DIContainer to Use Core Data Repository
|
|
|
|
**File:** `PlantGuide/Core/DI/DIContainer.swift`
|
|
|
|
**Do:**
|
|
```swift
|
|
// Find plantRepository property
|
|
// BEFORE:
|
|
var plantRepository: PlantRepositoryProtocol {
|
|
return InMemoryPlantRepository.shared
|
|
}
|
|
|
|
// AFTER:
|
|
var plantRepository: PlantRepositoryProtocol {
|
|
return _coreDataPlantStorage.value
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Check plantRepository returns Core Data
|
|
grep -A 3 "var plantRepository:" PlantGuide/Core/DI/DIContainer.swift
|
|
# Should reference coreData, not InMemory
|
|
|
|
# Run tests
|
|
xcodebuild test -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | grep -E "(Test Suite|passed|failed)"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] `plantRepository` returns Core Data storage
|
|
- [ ] All tests pass
|
|
- [ ] App displays persisted data correctly
|
|
|
|
**Commit:** `refactor(di): switch plantRepository to Core Data storage`
|
|
|
|
---
|
|
|
|
### Task 2.3.1: Guard Sample Data Seeding
|
|
|
|
**File:** `PlantGuide/Data/Repositories/InMemoryPlantRepository.swift`
|
|
|
|
**Do:**
|
|
```swift
|
|
// Find seedWithSampleData() call in init
|
|
// Wrap with DEBUG flag:
|
|
private init() {
|
|
#if DEBUG
|
|
seedWithSampleData()
|
|
#endif
|
|
}
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Check DEBUG guard exists
|
|
grep -A 5 "private init()" PlantGuide/Data/Repositories/InMemoryPlantRepository.swift
|
|
# Should show #if DEBUG around seedWithSampleData
|
|
|
|
# Build release config
|
|
xcodebuild -scheme PlantGuide -configuration Release -destination 'platform=iOS Simulator,name=iPhone 16' build 2>&1 | grep "BUILD SUCCEEDED"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] `seedWithSampleData()` wrapped in `#if DEBUG`
|
|
- [ ] Release build succeeds
|
|
|
|
**Commit:** `fix(data): guard sample data seeding with DEBUG flag`
|
|
|
|
---
|
|
|
|
## PHASE 3: Clean Architecture Compliance
|
|
|
|
### Task 3.1.1: Create UI Extensions File
|
|
|
|
**Do:**
|
|
```bash
|
|
mkdir -p PlantGuide/Presentation/Extensions
|
|
touch PlantGuide/Presentation/Extensions/Enums+UI.swift
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
ls PlantGuide/Presentation/Extensions/Enums+UI.swift
|
|
# Should exist
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] File created at correct path
|
|
|
|
**Commit:** N/A (file creation only)
|
|
|
|
---
|
|
|
|
### Task 3.1.2: Move UI Extensions from Domain
|
|
|
|
**Files:**
|
|
- Source: `PlantGuide/Domain/Entities/Enums.swift`
|
|
- Destination: `PlantGuide/Presentation/Extensions/Enums+UI.swift`
|
|
|
|
**Do:**
|
|
1. Copy these extensions to new file:
|
|
- `CareTaskType.iconName`
|
|
- `CareTaskType.iconColor`
|
|
- `CareTaskType.description`
|
|
- `LightRequirement.description`
|
|
- `WateringFrequency.description`
|
|
- `FertilizerFrequency.description`
|
|
- `HumidityLevel.description`
|
|
|
|
2. Remove from original file
|
|
3. Add `import SwiftUI` to new file only
|
|
|
|
**Validation:**
|
|
```bash
|
|
# No SwiftUI in Domain Enums
|
|
grep "import SwiftUI" PlantGuide/Domain/Entities/Enums.swift
|
|
# Should return nothing
|
|
|
|
# SwiftUI in Presentation extension
|
|
grep "import SwiftUI" PlantGuide/Presentation/Extensions/Enums+UI.swift
|
|
# Should return the import
|
|
|
|
# Build succeeds
|
|
xcodebuild -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' build 2>&1 | grep "BUILD SUCCEEDED"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] `Domain/Entities/Enums.swift` has no SwiftUI import
|
|
- [ ] All UI extensions live in `Presentation/Extensions/Enums+UI.swift`
|
|
- [ ] Build succeeds
|
|
- [ ] UI displays correctly (icons, colors visible)
|
|
|
|
**Commit:** `refactor(architecture): extract UI extensions from domain enums to presentation layer`
|
|
|
|
---
|
|
|
|
### Task 3.2.1: Document Existing Singletons
|
|
|
|
**Do:**
|
|
Create list of all `.shared` singletons:
|
|
|
|
```bash
|
|
grep -r "\.shared" PlantGuide/ --include="*.swift" | grep -v "Tests" | grep -v ".build"
|
|
```
|
|
|
|
**Validation:**
|
|
Document findings in this task:
|
|
- [ ] `DIContainer.shared` - Location: _____
|
|
- [ ] `CoreDataStack.shared` - Location: _____
|
|
- [ ] `InMemoryPlantRepository.shared` - Location: _____
|
|
- [ ] `FilterPreferencesStorage.shared` - Location: _____
|
|
- [ ] Other: _____
|
|
|
|
**Done When:**
|
|
- [ ] All singletons documented
|
|
|
|
**Commit:** N/A (documentation task)
|
|
|
|
---
|
|
|
|
## PHASE 4: Code Cleanup
|
|
|
|
### Task 4.1.1: Verify Item.swift is Unused
|
|
|
|
**Do:**
|
|
```bash
|
|
# Search for any reference to Item type
|
|
grep -r "Item" PlantGuide/ --include="*.swift" | grep -v "Item.swift" | grep -v "MenuItem" | grep -v "ListItem" | grep -v "// Item"
|
|
```
|
|
|
|
**Validation:**
|
|
- [ ] No meaningful references to `Item` type found
|
|
|
|
**Done When:**
|
|
- [ ] Confirmed `Item.swift` is dead code
|
|
|
|
**Commit:** N/A (verification only)
|
|
|
|
---
|
|
|
|
### Task 4.1.2: Delete Item.swift
|
|
|
|
**Do:**
|
|
```bash
|
|
rm PlantGuide/Item.swift
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
# File should not exist
|
|
ls PlantGuide/Item.swift 2>&1 | grep "No such file"
|
|
|
|
# Build succeeds
|
|
xcodebuild -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' build 2>&1 | grep "BUILD SUCCEEDED"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] File deleted
|
|
- [ ] Build succeeds
|
|
|
|
**Commit:** `chore: remove unused Item.swift template file`
|
|
|
|
---
|
|
|
|
### Task 4.2.1: Document @unchecked Sendable Usage
|
|
|
|
**Do:**
|
|
```bash
|
|
grep -rn "@unchecked Sendable" PlantGuide/ --include="*.swift"
|
|
```
|
|
|
|
For each occurrence, add a comment explaining WHY it's safe:
|
|
|
|
```swift
|
|
// MARK: - Thread Safety
|
|
// This type is @unchecked Sendable because:
|
|
// - All mutable state is protected by [mechanism]
|
|
// - Public interface is thread-safe because [reason]
|
|
@unchecked Sendable
|
|
```
|
|
|
|
**Validation:**
|
|
```bash
|
|
# Each @unchecked Sendable should have a comment within 5 lines above it
|
|
grep -B 5 "@unchecked Sendable" PlantGuide/Data/DataSources/Local/CoreData/CoreDataStack.swift | grep -E "(Thread Safety|thread-safe|Sendable because)"
|
|
```
|
|
|
|
**Done When:**
|
|
- [ ] Every `@unchecked Sendable` has justification comment
|
|
- [ ] No hidden thread-safety bugs identified
|
|
|
|
**Commit:** `docs(concurrency): document thread-safety justification for @unchecked Sendable types`
|
|
|
|
---
|
|
|
|
## Completion Checklist
|
|
|
|
### Phase 1 (Critical - Do First)
|
|
- [ ] 1.1.1 - Protocol conformance declaration
|
|
- [ ] 1.1.2 - Missing protocol methods
|
|
- [ ] 1.1.3 - Protocol conformance test
|
|
- [ ] 1.2.1 - Identify thread safety issue
|
|
- [ ] 1.2.2 - Fix thread safety
|
|
- [ ] 1.3.1 - Find unowned references
|
|
- [ ] 1.3.2 - Replace unowned with weak
|
|
|
|
### Phase 2 (High Priority)
|
|
- [ ] 2.1.1 - Audit SwiftData
|
|
- [ ] 2.1.2 - Remove unused SwiftData
|
|
- [ ] 2.2.1 - Verify CoreDataPlantStorage conformance
|
|
- [ ] 2.2.2 - Update DIContainer
|
|
- [ ] 2.3.1 - Guard sample data
|
|
|
|
### Phase 3 (Medium Priority)
|
|
- [ ] 3.1.1 - Create UI extensions file
|
|
- [ ] 3.1.2 - Move UI extensions
|
|
- [ ] 3.2.1 - Document singletons
|
|
|
|
### Phase 4 (Low Priority)
|
|
- [ ] 4.1.1 - Verify Item.swift unused
|
|
- [ ] 4.1.2 - Delete Item.swift
|
|
- [ ] 4.2.1 - Document @unchecked Sendable
|
|
|
|
---
|
|
|
|
## Final Validation
|
|
|
|
After all tasks complete:
|
|
|
|
```bash
|
|
# Full build
|
|
xcodebuild -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' clean build 2>&1 | tail -5
|
|
|
|
# Full test suite
|
|
xcodebuild test -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 16' 2>&1 | grep -E "Test Suite.*passed"
|
|
|
|
# No unowned self
|
|
grep -r "unowned self" PlantGuide/ --include="*.swift" | wc -l
|
|
# Should be 0
|
|
|
|
# No SwiftUI in Domain
|
|
grep -r "import SwiftUI" PlantGuide/Domain/ --include="*.swift" | wc -l
|
|
# Should be 0
|
|
```
|
|
|
|
**All Done When:**
|
|
- [ ] Clean build succeeds
|
|
- [ ] All tests pass
|
|
- [ ] Zero `unowned self` in codebase
|
|
- [ ] Zero SwiftUI imports in Domain layer
|
|
- [ ] App runs correctly on simulator
|
|
|
|
---
|
|
|
|
*Plan created: 2026-01-23*
|