# PlantGuide Feature Roadmap Implementation plan for 8 new features: Dark Mode, CloudKit Sync, Plant Rooms, Animations, Flexible Snoozing, Today View, Batch Actions, and Progress Photos. --- ## Requirements Summary | Feature | Specification | |---------|---------------| | **Dark Mode** | System-following + manual toggle, new color token system | | **CloudKit** | `NSPersistentCloudKitContainer`, fresh installs only | | **Rooms** | User-creatable `Room` entity, defaults: Kitchen, Living Room, Bedroom, Bathroom, Office, Patio/Balcony, Other | | **Snoozing** | 3 days, 1 week, 2 weeks, 1 month options | | **Today View** | Replaces Care tab, shows overdue + today's tasks | | **Batch Actions** | "Water all" for room or selection, logs per-plant | | **Progress Photos** | Scheduled reminders, time-lapse with user-controllable speed slider | | **Animations** | Subtle, defining—specified per context below | **Tab structure after implementation:** Camera | Browse | Collection | Today | Settings --- ## Implementation Order | Phase | Feature | Effort | Notes | |-------|---------|--------|-------| | **1** | Dark Mode + Color Tokens | S | Foundation for all UI | | **2** | CloudKit Sync | M | Must happen before new entities to avoid migration pain | | **3** | Plant Rooms/Zones | M | New `Room` entity (synced), user-creatable with defaults | | **4** | Subtle Animations | M | Design system completion | | **5** | Flexible Snoozing | S | Extend existing CareTask system | | **6** | Today View | M | Replace Care tab | | **7** | Batch Actions | S | Builds on rooms | | **8** | Progress Photos | L | New entity, reminders, time-lapse playback | --- ## Dependency Graph ``` Dark Mode (color tokens) ─────────────────┐ ├──► All UI features depend on this Subtle Animations ────────────────────────┘ Plant Rooms/Zones ──► Batch Actions ("Water all in Kitchen") ──► Today View (group by room) Progress Photos ──► Independent (but benefits from animations) Flexible Snoozing ──► Independent (extends existing system) Today View ──► Requires rooms to be meaningful ──► Benefits from animations ``` --- ## Phase 1: Dark Mode + Color Tokens ### Technical Approach Create a centralized design system with semantic color tokens that respond to color scheme. ### Files to Create | File | Purpose | |------|---------| | `Core/DesignSystem/ColorTokens.swift` | Semantic colors (background, surface, textPrimary, textSecondary, accent, destructive, etc.) | | `Core/DesignSystem/AppearanceManager.swift` | `@Observable` class managing appearance preference (system/light/dark) | | `Core/DesignSystem/DesignSystem.swift` | Namespace for tokens + spacing + typography | ### Files to Modify | File | Change | |------|--------| | `PlantGuideApp.swift` | Inject `AppearanceManager`, apply `.preferredColorScheme()` | | `MainTabView.swift` | Replace `.tint(.green)` with `DesignSystem.Colors.accent` | | `SettingsView.swift` | Add appearance picker (System/Light/Dark) | | All Views | Replace hardcoded colors with semantic tokens | ### Color Token Structure ```swift enum Colors { static let background = Color("Background") // Primary background static let surface = Color("Surface") // Cards, sheets static let textPrimary = Color("TextPrimary") static let textSecondary = Color("TextSecondary") static let accent = Color("Accent") // Green static let destructive = Color("Destructive") static let taskWatering = Color("TaskWatering") static let taskFertilizing = Color("TaskFertilizing") // ... etc } ``` ### Risks & Mitigations | Risk | Mitigation | |------|------------| | Inconsistent adoption | Grep for hardcoded `Color(` and `.foregroundColor` after implementation | | Asset catalog bloat | Use programmatic colors with `UIColor { traitCollection in }` | ### Tests - Unit: `AppearanceManager` persists preference correctly - UI: Screenshot tests for light/dark variants of key screens --- ## Phase 2: CloudKit Sync ### Technical Approach Replace `NSPersistentContainer` with `NSPersistentCloudKitContainer`. Enable automatic sync. ### Files to Modify | File | Change | |------|--------| | `CoreDataStack.swift` | Switch to `NSPersistentCloudKitContainer`, configure CloudKit container | | `PlantGuideModel.xcdatamodeld` | Enable "Used with CloudKit" on configuration | | `PlantGuide.entitlements` | Add CloudKit entitlement + iCloud container | | `Info.plist` | Add `NSUbiquitousContainerIdentifier` if needed | ### Xcode Configuration 1. Enable CloudKit capability in Signing & Capabilities 2. Create CloudKit container: `iCloud.com.yourteam.PlantGuide` 3. Enable "CloudKit" checkbox on Core Data model configuration ### Schema Considerations - All entities must have no unique constraints (CloudKit limitation) - All relationships must have inverses - No `Undefined` attribute types - Optional attributes for any field that might be nil during sync ### Risks & Mitigations | Risk | Mitigation | |------|------------| | Sync conflicts | CloudKit uses last-writer-wins; acceptable for this app | | Large photos slow sync | Store photos in `CKAsset` (handled automatically by Core Data) | | Offline-first gaps | `NSPersistentCloudKitContainer` handles this automatically | | Rate limiting | Monitor CloudKit dashboard; unlikely for personal use | ### Tests - Integration: Create plant on device A, verify appears on device B - Unit: Mock `NSPersistentCloudKitContainer` for offline behavior --- ## Phase 3: Plant Rooms/Zones ### Technical Approach New `Room` entity with one-to-many relationship to `Plant`. Replace `PlantLocation` enum usage. ### Files to Create | File | Purpose | |------|---------| | `Domain/Entities/Room.swift` | `Room` struct (id, name, icon, sortOrder, isDefault) | | `Domain/UseCases/Room/CreateDefaultRoomsUseCase.swift` | Creates 7 default rooms on first launch | | `Domain/UseCases/Room/ManageRoomsUseCase.swift` | CRUD for rooms | | `Domain/RepositoryInterfaces/RoomRepositoryProtocol.swift` | Repository protocol | | `Data/Repositories/CoreDataRoomRepository.swift` | Core Data implementation | | `Presentation/Scenes/Rooms/RoomsListView.swift` | Settings screen to manage rooms | | `Presentation/Scenes/Rooms/RoomEditorView.swift` | Create/edit room | | `Presentation/Scenes/Rooms/RoomsViewModel.swift` | ViewModel | ### Files to Modify | File | Change | |------|--------| | `PlantGuideModel.xcdatamodeld` | Add `RoomMO` entity, relationship to `PlantMO` | | `Plant.swift` | Change `location: PlantLocation?` to `roomID: UUID?` | | `PlantMO` | Add `room` relationship | | `CollectionView.swift` | Add room filter/grouping option | | `PlantDetailView.swift` | Room picker instead of location enum | | `SettingsView.swift` | Add "Manage Rooms" row | | `DIContainer.swift` | Register room repository and use cases | ### Default Rooms ```swift let defaults = [ Room(name: "Kitchen", icon: "refrigerator", sortOrder: 0, isDefault: true), Room(name: "Living Room", icon: "sofa", sortOrder: 1, isDefault: true), Room(name: "Bedroom", icon: "bed.double", sortOrder: 2, isDefault: true), Room(name: "Bathroom", icon: "shower", sortOrder: 3, isDefault: true), Room(name: "Office", icon: "desktopcomputer", sortOrder: 4, isDefault: true), Room(name: "Patio/Balcony", icon: "sun.max", sortOrder: 5, isDefault: true), Room(name: "Other", icon: "square.grid.2x2", sortOrder: 6, isDefault: true), ] ``` ### Risks & Mitigations | Risk | Mitigation | |------|------------| | Orphaned plants when room deleted | Cascade to "Other" room or prompt user | | Migration from `PlantLocation` enum | Map existing enum values to default rooms in migration | ### Tests - Unit: `CreateDefaultRoomsUseCase` creates exactly 7 rooms - Unit: Room deletion cascades plants correctly - UI: Room picker appears in plant detail --- ## Phase 4: Subtle Animations ### Technical Approach Create reusable animation modifiers and view extensions. Apply consistently. ### Files to Create | File | Purpose | |------|---------| | `Core/DesignSystem/Animations.swift` | Standard timing curves, durations | | `Presentation/Common/Modifiers/FadeInModifier.swift` | Staggered fade-in for lists | | `Presentation/Common/Modifiers/TaskCompleteModifier.swift` | Checkmark + confetti burst | | `Presentation/Common/Modifiers/WateringFeedbackModifier.swift` | Water droplet animation | | `Presentation/Common/Modifiers/PlantGrowthModifier.swift` | Subtle scale pulse | | `Presentation/Common/Components/SuccessCheckmark.swift` | Animated checkmark component | ### Animation Specifications | Context | Animation | |---------|-----------| | **Task complete** | Checkmark draws in (0.3s), row scales down + fades (0.2s), optional confetti particles | | **Watering feedback** | 3 water droplets fall and fade (0.5s staggered) | | **List appearance** | Staggered fade-in, 0.05s delay per item, max 10 items animated | | **Tab switch** | Cross-fade (0.2s) | | **Card press** | Scale to 0.97 on press, spring back | | **Pull to refresh** | Plant icon rotates while loading | | **Empty state** | Gentle float animation on illustration | ### Timing Standards ```swift enum Animations { static let quick = Animation.easeOut(duration: 0.15) static let standard = Animation.easeInOut(duration: 0.25) static let emphasis = Animation.spring(response: 0.4, dampingFraction: 0.7) static let stagger = 0.05 // seconds between items } ``` ### Files to Modify | File | Change | |------|--------| | `CareTaskRow.swift` | Add completion animation | | `CollectionView.swift` | Staggered list animation | | `MainTabView.swift` | Tab transition | | All cards/buttons | Press feedback | ### Risks & Mitigations | Risk | Mitigation | |------|------------| | Animation jank on older devices | Use `Animation.interactiveSpring` which degrades gracefully | | Overdone animations | Keep durations under 0.5s, use subtle scales (0.95-1.05 range) | | Accessibility | Respect `UIAccessibility.isReduceMotionEnabled` | ### Tests - Unit: Animations respect reduce motion setting - Manual: Test on oldest supported device (iPhone XR or similar) --- ## Phase 5: Flexible Snoozing ### Technical Approach Extend existing snooze system with new intervals. Add snooze picker UI. ### Files to Create | File | Purpose | |------|---------| | `Presentation/Common/Components/SnoozePickerView.swift` | Bottom sheet with snooze options | ### Files to Modify | File | Change | |------|--------| | `CareTask.swift` | Add `snoozedUntil: Date?` property, `snoozeCount: Int` | | `CareTaskRow.swift` | Replace swipe actions with snooze picker trigger | | `CareScheduleViewModel.swift` | Update `snoozeTask()` to accept `SnoozeInterval` enum | | `NotificationService.swift` | Reschedule notification on snooze | | `CareTaskMO` | Add `snoozedUntil` and `snoozeCount` attributes | ### Snooze Options ```swift enum SnoozeInterval: CaseIterable { case threeDays case oneWeek case twoWeeks case oneMonth var days: Int { switch self { case .threeDays: return 3 case .oneWeek: return 7 case .twoWeeks: return 14 case .oneMonth: return 30 } } var label: String { switch self { case .threeDays: return "3 days" case .oneWeek: return "1 week" case .twoWeeks: return "2 weeks" case .oneMonth: return "1 month" } } } ``` ### UI Behavior - Swipe left on task → shows snooze picker (bottom sheet) - Picker shows 4 options + "Custom date" option - Snoozed tasks show "Snoozed until [date]" badge - Snoozed tasks appear in "Snoozed" filter section ### Risks & Mitigations | Risk | Mitigation | |------|------------| | Users abuse snooze | Track `snoozeCount`, optionally show "frequently snoozed" indicator | | Notification sync | Cancel old notification, schedule new one in same transaction | ### Tests - Unit: `snoozeTask(interval:)` calculates correct future date - Unit: Snooze count increments - UI: Snoozed task appears in correct section --- ## Phase 6: Today View ### Technical Approach New view replacing Care tab. Dashboard with overdue + today sections, grouped by room. ### Files to Create | File | Purpose | |------|---------| | `Presentation/Scenes/TodayView/TodayView.swift` | Main dashboard | | `Presentation/Scenes/TodayView/TodayViewModel.swift` | ViewModel | | `Presentation/Scenes/TodayView/Components/TaskSection.swift` | Overdue/Today section | | `Presentation/Scenes/TodayView/Components/RoomTaskGroup.swift` | Tasks grouped by room | | `Presentation/Scenes/TodayView/Components/QuickStatsBar.swift` | Today's progress (3/7 tasks done) | ### Files to Modify | File | Change | |------|--------| | `MainTabView.swift` | Replace Care tab with Today tab | | `DIContainer.swift` | Add `makeTodayViewModel()` | ### Today View Layout ``` ┌─────────────────────────────────┐ │ Good morning! │ │ 3 of 7 tasks completed │ ├─────────────────────────────────┤ │ OVERDUE (2) │ │ ┌─────────────────────────────┐ │ │ │ Monstera - Water (2d ago) │ │ │ │ Fern - Fertilize (1d ago) │ │ │ └─────────────────────────────┘ │ ├─────────────────────────────────┤ │ TODAY (5) │ │ │ │ Kitchen (2) │ │ ┌─────────────────────────────┐ │ │ │ Pothos - Water │ │ │ │ Basil - Water │ │ │ └─────────────────────────────┘ │ │ │ │ Living Room (3) │ │ ┌─────────────────────────────┐ │ │ │ Snake Plant - Water │ │ │ │ Spider Plant - Fertilize │ │ │ │ Cactus - Water │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────┘ ``` ### Risks & Mitigations | Risk | Mitigation | |------|------------| | Empty state confusion | Show encouraging empty state: "All caught up!" | | Performance with many tasks | Limit visible tasks, add "Show all" expansion | ### Tests - Unit: Tasks correctly grouped by overdue/today/room - UI: Empty state displays correctly - UI: Task completion updates stats bar --- ## Phase 7: Batch Actions ### Technical Approach Multi-select mode + batch action bar. Actions apply to selection or entire room. ### Files to Create | File | Purpose | |------|---------| | `Presentation/Common/Components/BatchActionBar.swift` | Floating action bar | | `Domain/UseCases/PlantCare/BatchCompleteTasksUseCase.swift` | Complete multiple tasks | ### Files to Modify | File | Change | |------|--------| | `TodayView.swift` | Add edit mode, selection state | | `TodayViewModel.swift` | `selectedTaskIDs: Set`, batch action methods | | `RoomTaskGroup.swift` | "Water all in [Room]" button | | `CareScheduleRepositoryProtocol.swift` | Add `batchUpdate(taskIDs:completion:)` | ### UI Behavior 1. **Room-level batch**: Each room group has "Water all" button (only for watering tasks) 2. **Selection mode**: Long-press or Edit button enables multi-select 3. **Batch bar**: Appears at bottom when items selected: "Complete (5)" button 4. **Logging**: Each plant gets individual `CareTask` completion entry ### Batch Action Bar ``` ┌─────────────────────────────────┐ │ 5 selected [Complete] [Cancel]│ └─────────────────────────────────┘ ``` ### Risks & Mitigations | Risk | Mitigation | |------|------------| | Accidental batch complete | Require confirmation for >3 tasks | | Mixed task types in selection | Only show "Complete" (generic), not task-specific action | ### Tests - Unit: `BatchCompleteTasksUseCase` creates individual completion records - UI: Selection count updates correctly - UI: Confirmation appears for large batches --- ## Phase 8: Progress Photos ### Technical Approach New `ProgressPhoto` entity linked to `Plant`. Scheduled reminders for capture. Time-lapse viewer with speed control. ### Files to Create | File | Purpose | |------|---------| | `Domain/Entities/ProgressPhoto.swift` | Photo entity (id, plantID, imageData, dateTaken, notes) | | `Domain/RepositoryInterfaces/ProgressPhotoRepositoryProtocol.swift` | Repository protocol | | `Data/Repositories/CoreDataProgressPhotoRepository.swift` | Core Data implementation | | `Domain/UseCases/Photos/CaptureProgressPhotoUseCase.swift` | Save photo with metadata | | `Domain/UseCases/Photos/SchedulePhotoReminderUseCase.swift` | Weekly/monthly reminder | | `Presentation/Scenes/ProgressPhotos/ProgressPhotoGalleryView.swift` | Grid of photos for plant | | `Presentation/Scenes/ProgressPhotos/ProgressPhotoCaptureView.swift` | Camera capture screen | | `Presentation/Scenes/ProgressPhotos/TimeLapsePlayerView.swift` | Time-lapse viewer | | `Presentation/Scenes/ProgressPhotos/ProgressPhotosViewModel.swift` | ViewModel | ### Files to Modify | File | Change | |------|--------| | `PlantGuideModel.xcdatamodeld` | Add `ProgressPhotoMO` entity | | `PlantDetailView.swift` | Add "Progress Photos" section | | `NotificationService.swift` | Add photo reminder notification type | | `DIContainer.swift` | Register photo repositories and use cases | ### Core Data Entity ``` ProgressPhotoMO ├── id: UUID ├── plantID: UUID ├── imageData: Binary Data (External Storage) ├── thumbnailData: Binary Data ├── dateTaken: Date ├── notes: String? └── plant: PlantMO (relationship) ``` ### Time-Lapse Player ``` ┌─────────────────────────────────┐ │ [Photo Display] │ │ │ │ ◀ ▶ Jan 15, 2026 │ │ │ │ Speed: [━━━━●━━━━━] 0.3s/frame │ │ │ │ [ Play ] │ └─────────────────────────────────┘ ``` ### Photo Reminder Options - Weekly (same day each week) - Bi-weekly - Monthly (same date each month) - Off ### Storage Strategy - Full resolution stored as `Binary Data` with "Allows External Storage" enabled - Thumbnail (200x200) stored inline for fast gallery loading - CloudKit syncs via `CKAsset` automatically ### Risks & Mitigations | Risk | Mitigation | |------|------------| | Storage bloat | Compress to HEIC, limit resolution to 2048px max dimension | | CloudKit photo sync slow | Show sync indicator, allow offline viewing of local photos | | Gallery performance | Load thumbnails only, full image on tap | ### Tests - Unit: Photo saved with correct metadata - Unit: Thumbnail generated at correct size - UI: Time-lapse plays at correct speed - Integration: Photo syncs to CloudKit --- ## Red Team Check ### Potential Issues Across All Phases | Issue | Phase | Severity | Mitigation | |-------|-------|----------|------------| | CloudKit container misconfigured | 2 | Critical | Test on real device early, not just simulator | | Color tokens not adopted everywhere | 1 | Medium | Add SwiftLint rule or grep check | | Room deletion orphans data | 3 | High | Cascade to "Other" room, never delete "Other" | | Animation performance on old devices | 4 | Medium | Test on iPhone XR/XS, use `interactiveSpring` | | Snooze notifications don't update | 5 | High | Wrap in transaction: cancel + reschedule | | Today view empty state confusing | 6 | Low | Design clear empty states early | | Batch action logging wrong | 7 | Medium | Unit test verifies individual records | | Photo storage exceeds iCloud quota | 8 | Medium | Warn user when approaching limit | ### Rollback Strategy Each phase is independently deployable. If issues arise: 1. **Feature flags**: Wrap new features in `@AppStorage("feature_X_enabled")` flags 2. **Database migrations**: All Core Data changes are additive (new entities/attributes), never destructive 3. **CloudKit schema**: Once deployed to production, schema can only be extended, not modified --- ## Totals | Phase | Feature | New Files | Modified Files | Effort | |-------|---------|-----------|----------------|--------| | 1 | Dark Mode | 3 | ~15 | S | | 2 | CloudKit | 0 | 4 | M | | 3 | Rooms | 8 | 7 | M | | 4 | Animations | 6 | 5 | M | | 5 | Snoozing | 1 | 5 | S | | 6 | Today View | 5 | 2 | M | | 7 | Batch Actions | 2 | 4 | S | | 8 | Progress Photos | 9 | 4 | L | **Total: ~34 new files, ~30 modified files**