Reminora (Wahi) SwiftUI Development
Expert guidance for working with the Reminora codebase, a geotagged photo sharing app with SwiftUI (iOS) and Kotlin (Android) implementations.
What This Skill Does
Provides comprehensive context and best practices for developing Reminora, including:
SwiftUI navigation architecture with modern NavigationStack patternsService-based architecture with dependency injectionCore Data models and cloud synchronizationPhoto management with smart stacking and similarity detectionDeep link handling and pin sharingSocial features (following, comments, user profiles)Map interaction with pin clusteringBackend integration with Cloudflare WorkersProject Overview
Reminora is a geotagged photo sharing app that allows users to:
View their photo library with smart stackingSave photos with location dataView photos on an interactive mapShare pins with other usersFollow users and comment on pinsSync data across devices via cloud**Stack:**
iOS: SwiftUI, Core Data, MapKit, Photos frameworkAndroid: Kotlin, Room database, MVVMBackend: Cloudflare Workers, D1 SQLite, TypeScriptDevelopment Commands
Build and Run
```bash
Build main app
xcodebuild -project reminora.xcodeproj -scheme reminora -configuration Debug build
Run tests
xcodebuild -project reminora.xcodeproj -scheme reminora -configuration Debug test -destination 'platform=iOS Simulator,name=iPhone 15'
Build share extension
xcodebuild -project reminora.xcodeproj -scheme ReminoraShareExt -configuration Debug build
Open in Xcode
open reminora.xcodeproj
```
Architecture Guidelines
1. Core Components
**Main App (`ios/reminora/`):**
`reminoraApp.swift` - App entry point with Core Data setup`ContentView.swift` - Main navigation controller with route-based navigation`PinMainView.swift` - Interactive map with sliding panel`PhotoMainView.swift` - Photo library browser`SwipePhotoView.swift` - Full-screen photo viewer`AddPinFromPhotoView.swift` - Create pins with reverse geocoding**Cloud Services (`ios/reminora/cloud/`):**
`AuthenticationService.swift` - User authentication and sessions`CloudSyncService.swift` - Local/cloud data synchronization`APIService.swift` - HTTP API client`PinSharingService.swift` - Deep link handling and pin sharing`UserProfileView.swift` - User profile management**Pin Management (`ios/reminora/pin/`):**
`PinDetailView.swift` - Detailed pin view`SelectLocationsView.swift` - Multi-select location picker`NearbyLocationsPageView.swift` - Explore nearby places`CommentsView.swift` - Pin comments and interactions2. Service-Based Architecture
**Core Services (Singletons):**
**SelectionService** (`shared/SelectionService.swift`):
Manages multi-selection state for photos globallyUsage: `@Environment(\.selectedAssetService)` or `SelectionService.shared`Key methods: `addSelectedPhoto()`, `removeSelectedPhoto()`, `isPhotoSelected()`Observable with `@Published` properties**ToolbarManager** (`shared/DynamicToolbar.swift`):
Manages dynamic toolbar state and configurationsUsage: `@Environment(\.toolbarManager)`Methods: `setFABOnlyMode()`, `setCustomToolbar()`, `updateCustomToolbar()`**SheetStack** (`shared/SheetStack.swift`):
Centralized sheet presentation with stack-based navigationUsage: `@Environment(\.sheetStack)` or `SheetStack.shared`Methods: `push()`, `pop()`, `clear()`**UniversalActionSheetModel** (`shared/UniversalActionSheetModel.swift`):
Context-aware action sheet with dynamic contentUsage: `UniversalActionSheetModel.shared`Method: `setContext(ActionSheetContext)`**AuthenticationService** (`cloud/AuthenticationService.swift`):
User authentication and session managementUsage: `@EnvironmentObject private var authService: AuthenticationService`Properties: `currentAccount`, `authState`**ClipManager** (`clip/Clip.swift`):
Manages clip creation, editing, and persistenceUsage: `@Environment(\.clipManager)` or `ClipManager.shared`Dual storage: UserDefaults + RListData integration**ECardEditor** (`ecard/ECardEditor.swift`):
Manages ECard editing sessionsUsage: `@Environment(\.eCardEditor)` or `ECardEditor.shared`**ActionRouter** (`shared/ActionRouter.swift`):
Centralized action execution with dependency injectionUsage: `ActionRouter.shared.execute(ActionType)`Requires setup with SheetStack, SelectionService, ToolbarManager**PinSharingService** (`cloud/PinSharingService.swift`):
Deep link handling and pin sharingUsage: `PinSharingService.shared`Methods: `handleReminoraLink()`, `sharePin()`, `isSharedFromOtherUser()`3. Navigation System
**Modern NavigationStack Architecture:**
Uses iOS 16+ NavigationStack with route-based navigation`AppRoute` enum defines all main destinationsCustom bottom toolbar with context-aware buttonsUniversal FAB (floating action button) for quick actions**Navigation Routes:**
1. **Photos Route**: PhotoMainView with FAB-only mode
2. **Map Route**: MapView with navigation toolbar
3. **Pins Route**: PinMainView with pin actions
4. **Lists Route**: AllRListsView with refresh
5. **Profile Route**: ProfileView with FAB-only mode
6. **Editor Routes**: ECard and Clip editors
**Universal FAB:**
Icon: "r.circle.fill" (blue circular button)Position: Bottom of screen (centered or in toolbar)Action: Opens UniversalActionSheet with context-aware actions**IMPORTANT - SwipePhotoView Integration:**
MUST restore toolbar state on dismissalUse `NotificationCenter.default.post(name: NSNotification.Name("RestoreToolbar"), object: nil)`ContentView listens and calls `setupToolbarForTab(selectedTab)`Scroll position preservation via "RestoreScrollPosition" notification4. Data Model
**Place Entity (Core Data):**
`imageData: Binary` - Downsampled JPEG`location: Binary` - Archived CLLocation with GPS`locations: String` - JSON array of PlaceAddress objects`dateAdded: Date` - When photo was added`post: String` - Caption/text`url: String` - Original file URL`cloudId: String` - Cloud sync ID`isPrivate: Bool` - Privacy setting`originalUserId: String` - Original creator ID (for shared pins)`originalUsername: String` - Creator username`originalDisplayName: String` - Creator display name**Additional Entities:**
`Comment` - User comments on pins`UserList` - Following relationships`ListItem` - Items within lists5. Common Patterns
**Core Data Access:**
```swift
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(entity: Place.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Place.dateAdded, ascending: false)])
private var places: FetchedResults<Place>
```
**Service Integration:**
```swift
// Environment injection
.environment(\.toolbarManager, toolbarManager)
.environment(\.selectedAssetService, selectedAssetService)
.environment(\.sheetStack, sheetStack)
// Shared access
SelectionService.shared.addSelectedPhoto(assetId)
UniversalActionSheetModel.shared.setContext(.photos)
```
**LazySnapPager:**
Memory-efficient horizontal photo swipingRobust spring animations: `response: 0.4, dampingFraction: 0.8`Only renders previous/current/next imagesThreshold-based swipe detection**Notification Integration:**
"RestoreToolbar" - Restore toolbar after SwipePhotoView dismissal"RestoreScrollPosition" - Restore scroll position in PhotoMainViewUse for cross-service communication without tight couplingKey Features Implementation
Cloud Synchronization
Real-time sync between local Core Data and Cloudflare Workers backendConflict resolution with last-write-wins strategyOffline-first architecture with queue-based syncDeep Link Support
Comprehensive handling via `PinSharingService`Tracks original creator for shared pinsSupports pin sharing across usersIntegration in `reminoraApp.swift` and `ProfileView`Photo Management
Smart stacking with similarity detectionDynamic stack expanding/collapsingEXIF data extraction for locationLazy loading with `PHImageManager`Map Interaction
Pin clustering for performanceInteractive pins with detail viewsReverse geocoding for place namesMulti-address supportSocial Features
User following systemPin comments and interactionsUser profiles with activity feedsPrivacy controls (public/private pins)Development Best Practices
1. **Always use environment injection** for service access when possible
2. **Maintain reactive updates** with `@Published` properties
3. **Use NotificationCenter** for loose coupling between components
4. **Follow NavigationStack patterns** for proper navigation hierarchy
5. **Restore toolbar state** after full-screen view dismissals
6. **Use LazySnapPager** for photo swiping instead of custom implementations
7. **Implement proper error handling** for API calls and Core Data operations
8. **Test deep link handling** thoroughly (use ProfileView debug buttons)
9. **Preserve scroll positions** when navigating between views
10. **Use ActionRouter** for centralized action handling
Testing
```bash
Unit tests
xcodebuild -project reminora.xcodeproj -scheme reminora -configuration Debug test -destination 'platform=iOS Simulator,name=iPhone 15'
UI tests via Xcode Test navigator
Test files: reminoraTests/, reminoraUITests/
```
Technical Details
**iOS Requirements:**
Target: iOS 18.2+Swift 5.0, SwiftUIFrameworks: Core Data, MapKit, Photos, PhotosUI, Core Location, CoreLocationUIBundle ID: `com.alexezh.reminora`App Group: `group.com.alexezh.reminora`Permissions: Location (when in use), Photo Library access**Backend:**
Cloudflare Workers with TypeScriptD1 SQLite databaseSession-based auth with Bearer tokensRESTful API endpoints**Android:**
Kotlin with MVVM architectureRoom databaseMatching iOS functionalityCommon Issues and Solutions
**Issue: Toolbar disappears after SwipePhotoView dismissal**
Solution: Post "RestoreToolbar" notification in onDismiss handlerContentView listens and restores toolbar for current tab**Issue: Scroll position lost when returning to PhotoMainView**
Solution: Use "RestoreScrollPosition" notification with SwiftUI's `.scrollPosition(id:)`**Issue: Photo selection state not persisting across views**
Solution: Use `SelectionService.shared` for global selection management**Issue: Deep links not properly tracking original creator**
Solution: Use `PinSharingService` and ensure `originalUserId`, `originalUsername`, `originalDisplayName` are set**Issue: Images not snapping properly in LazySnapPager**
Solution: Use optimized spring animation parameters: `response: 0.4, dampingFraction: 0.8, blendDuration: 0`