- 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>
15 KiB
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:
// Find this line:
final class InMemoryPlantRepository { ... }
// Change to:
final class InMemoryPlantRepository: PlantRepositoryProtocol { ... }
Validation:
# 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
InMemoryPlantRepositoryexplicitly declaresPlantRepositoryProtocolconformance
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:
- Check what methods
PlantRepositoryProtocolrequires - Compare against what
InMemoryPlantRepositoryimplements - Add any missing methods
Validation:
# 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:
import XCTest
@testable import PlantGuide
final class InMemoryPlantRepositoryTests: XCTestCase {
func testConformsToPlantRepositoryProtocol() {
let repo: PlantRepositoryProtocol = InMemoryPlantRepository.shared
XCTAssertNotNil(repo)
}
}
Validation:
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:
- Read the file
- Find
lazy var persistentContainer - Document the race condition scenario
Validation:
# 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:
// Replace lazy var with let + init
// BEFORE:
lazy var persistentContainer: NSPersistentContainer = { ... }()
// AFTER:
let persistentContainer: NSPersistentContainer
init() {
persistentContainer = NSPersistentContainer(name: "PlantGuide")
persistentContainer.loadPersistentStores { ... }
}
Validation:
# 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 varforpersistentContainer - 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:
grep -n "unowned self" PlantGuide/Core/DI/DIContainer.swift
Validation:
# 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 selflocations 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:
// 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:
# 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 selfin 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.swiftPlantGuide/Item.swift
Do:
# 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.swiftPlantGuide/Item.swift
Do:
- Delete
Item.swift - In
PlantGuideApp.swift:- Remove
sharedModelContainerproperty - Remove
.modelContainer(sharedModelContainer)modifier - Remove
import SwiftData
- Remove
Validation:
# 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.swiftdeleted- 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:
grep -A 5 "class CoreDataPlantStorage" PlantGuide/Data/DataSources/Local/CoreData/
grep "PlantRepositoryProtocol" PlantGuide/Data/DataSources/Local/CoreData/CoreDataPlantStorage.swift
Validation:
CoreDataPlantStorageexplicitly conforms toPlantRepositoryProtocol
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:
// Find plantRepository property
// BEFORE:
var plantRepository: PlantRepositoryProtocol {
return InMemoryPlantRepository.shared
}
// AFTER:
var plantRepository: PlantRepositoryProtocol {
return _coreDataPlantStorage.value
}
Validation:
# 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:
plantRepositoryreturns 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:
// Find seedWithSampleData() call in init
// Wrap with DEBUG flag:
private init() {
#if DEBUG
seedWithSampleData()
#endif
}
Validation:
# 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:
mkdir -p PlantGuide/Presentation/Extensions
touch PlantGuide/Presentation/Extensions/Enums+UI.swift
Validation:
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:
-
Copy these extensions to new file:
CareTaskType.iconNameCareTaskType.iconColorCareTaskType.descriptionLightRequirement.descriptionWateringFrequency.descriptionFertilizerFrequency.descriptionHumidityLevel.description
-
Remove from original file
-
Add
import SwiftUIto new file only
Validation:
# 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.swifthas 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:
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:
# 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
Itemtype found
Done When:
- Confirmed
Item.swiftis dead code
Commit: N/A (verification only)
Task 4.1.2: Delete Item.swift
Do:
rm PlantGuide/Item.swift
Validation:
# 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:
grep -rn "@unchecked Sendable" PlantGuide/ --include="*.swift"
For each occurrence, add a comment explaining WHY it's safe:
// 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:
# 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 Sendablehas 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:
# 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 selfin codebase - Zero SwiftUI imports in Domain layer
- App runs correctly on simulator
Plan created: 2026-01-23