|
| 1 | +# Camera2URL Architecture and Code Design |
| 2 | + |
| 3 | +This document provides an overview of the Camera2URL codebase architecture, helping developers understand the project structure, design patterns, and code organization. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +Camera2URL is a native camera application for iOS and macOS that captures photos and uploads them to a configurable HTTP endpoint. The app supports: |
| 8 | + |
| 9 | +- Manual photo capture with immediate upload |
| 10 | +- Automatic timed capture at configurable intervals |
| 11 | +- Multiple camera selection (front/back on iOS, external/continuity on macOS) |
| 12 | +- Upload history tracking with request/response details |
| 13 | + |
| 14 | +## Repository Structure |
| 15 | + |
| 16 | +``` |
| 17 | +camera2url/ |
| 18 | +├── shared/ # Shared Swift Package (cross-platform code) |
| 19 | +├── ios/ # iOS application |
| 20 | +├── macos/ # macOS application |
| 21 | +└── docs/ # Documentation |
| 22 | +``` |
| 23 | + |
| 24 | +### Shared Package (`shared/`) |
| 25 | + |
| 26 | +A Swift Package containing all platform-agnostic business logic: |
| 27 | + |
| 28 | +``` |
| 29 | +shared/ |
| 30 | +├── Package.swift |
| 31 | +├── Sources/Camera2URLShared/ |
| 32 | +│ ├── Models/ # Data models (RequestConfig, TimerConfig, etc.) |
| 33 | +│ ├── Protocols/ # Platform abstraction protocols |
| 34 | +│ ├── Services/ # Network services (UploadService) |
| 35 | +│ ├── Stores/ # Persistence (ConfigStore) |
| 36 | +│ ├── ViewModels/ # Main app state (AppViewModel) |
| 37 | +│ └── PlatformImage.swift # Cross-platform image typealias |
| 38 | +└── Tests/ |
| 39 | +``` |
| 40 | + |
| 41 | +### Platform Apps (`ios/` and `macos/`) |
| 42 | + |
| 43 | +Each platform app contains only platform-specific code: |
| 44 | + |
| 45 | +``` |
| 46 | +{platform}/ |
| 47 | +├── camera2url.xcodeproj/ |
| 48 | +├── camera2url/ |
| 49 | +│ ├── Services/ # Platform-specific CameraService |
| 50 | +│ ├── Views/ # SwiftUI views |
| 51 | +│ ├── ContentView.swift |
| 52 | +│ └── camera2urlApp.swift |
| 53 | +└── camera2urlTests/ |
| 54 | +``` |
| 55 | + |
| 56 | +## Architecture Overview |
| 57 | + |
| 58 | +### Layer Diagram |
| 59 | + |
| 60 | +``` |
| 61 | +┌─────────────────────────────────────────────────────────────┐ |
| 62 | +│ SwiftUI Views │ |
| 63 | +│ (Platform-specific UI) │ |
| 64 | +├─────────────────────────────────────────────────────────────┤ |
| 65 | +│ AppViewModel │ |
| 66 | +│ (Shared state management) │ |
| 67 | +├───────────────────────┬─────────────────────────────────────┤ |
| 68 | +│ CameraService │ UploadService │ |
| 69 | +│ (Platform-specific) │ (Shared) │ |
| 70 | +├───────────────────────┴─────────────────────────────────────┤ |
| 71 | +│ Models & Stores │ |
| 72 | +│ (Shared) │ |
| 73 | +└─────────────────────────────────────────────────────────────┘ |
| 74 | +``` |
| 75 | + |
| 76 | +### Key Design Decisions |
| 77 | + |
| 78 | +1. **Protocol-based abstraction for platform code**: The `CameraServiceProtocol` defines the camera interface, allowing the shared `AppViewModel` to work with platform-specific camera implementations. |
| 79 | + |
| 80 | +2. **Generic ViewModel**: `AppViewModel` is generic over the camera service type, enabling type-safe platform-specific camera device handling while sharing all business logic. |
| 81 | + |
| 82 | +3. **Platform image typealias**: `PlatformImage` resolves to `UIImage` on iOS and `NSImage` on macOS, enabling the ViewModel to work with images without platform conditionals. |
| 83 | + |
| 84 | +## Code Sharing Strategy |
| 85 | + |
| 86 | +### What's Shared (~60% of code) |
| 87 | + |
| 88 | +| Component | Description | |
| 89 | +|-----------|-------------| |
| 90 | +| **Models** | All data structures (RequestConfig, TimerConfig, UploadRecord, etc.) | |
| 91 | +| **UploadService** | HTTP multipart upload logic | |
| 92 | +| **ConfigStore** | UserDefaults-based configuration persistence | |
| 93 | +| **AppViewModel** | All app state, timer logic, upload orchestration | |
| 94 | +| **Protocols** | CameraServiceProtocol, CameraDeviceInfo | |
| 95 | + |
| 96 | +### What's Platform-Specific |
| 97 | + |
| 98 | +| Component | Why Platform-Specific | |
| 99 | +|-----------|----------------------| |
| 100 | +| **CameraService** | Different device types (iOS: front/back cameras; macOS: external/continuity) | |
| 101 | +| **CameraPreviewView** | UIViewRepresentable vs NSViewRepresentable | |
| 102 | +| **Config UI** | iOS uses NavigationStack/Form; macOS uses dialog-style layout | |
| 103 | +| **ContentView** | Different layouts, safe areas, window management | |
| 104 | +| **UploadHistoryView** | iOS uses push navigation; macOS uses split view | |
| 105 | + |
| 106 | +## Key Abstractions |
| 107 | + |
| 108 | +### CameraServiceProtocol |
| 109 | + |
| 110 | +Defines the contract for camera operations: |
| 111 | + |
| 112 | +```swift |
| 113 | +protocol CameraServiceProtocol { |
| 114 | + associatedtype CameraDeviceType: CameraDeviceInfo |
| 115 | + |
| 116 | + var session: AVCaptureSession? { get } |
| 117 | + var availableCameras: [CameraDeviceType] { get } |
| 118 | + var currentCamera: CameraDeviceType? { get } |
| 119 | + var delegate: CameraServiceDelegate? { get set } |
| 120 | + |
| 121 | + func prepareIfNeeded() async throws |
| 122 | + func start() |
| 123 | + func stop() |
| 124 | + func capturePhoto() |
| 125 | + func switchCamera(to camera: CameraDeviceType) throws |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +### CameraServiceDelegate |
| 130 | + |
| 131 | +Callback interface for camera events: |
| 132 | + |
| 133 | +```swift |
| 134 | +protocol CameraServiceDelegate { |
| 135 | + func cameraServiceDidCapturePhoto(_ data: Data) |
| 136 | + func cameraServiceDidEncounterError(_ error: Error) |
| 137 | + func cameraServiceDidUpdateAvailableCameras() |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +### AppViewModel |
| 142 | + |
| 143 | +The central state manager, generic over the camera service: |
| 144 | + |
| 145 | +```swift |
| 146 | +class AppViewModel<CameraService: CameraServiceProtocol>: ObservableObject { |
| 147 | + // Published state for UI binding |
| 148 | + @Published var uploadStatus: UploadStatus |
| 149 | + @Published var isTimerActive: Bool |
| 150 | + @Published var capturedPhoto: CapturedPhoto? |
| 151 | + // ... etc |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +## Data Flow |
| 156 | + |
| 157 | +### Manual Capture Flow |
| 158 | + |
| 159 | +``` |
| 160 | +User taps "Take Photo" |
| 161 | + → AppViewModel.takeAndSendPhoto() |
| 162 | + → CameraService.capturePhoto() |
| 163 | + → [AVFoundation captures image] |
| 164 | + → CameraServiceDelegate.cameraServiceDidCapturePhoto(data) |
| 165 | + → AppViewModel creates PlatformImage |
| 166 | + → UploadService.upload(photoData:using:) |
| 167 | + → [HTTP request] |
| 168 | + → UploadHistory.addSuccess/addFailure() |
| 169 | + → UI updates via @Published |
| 170 | +``` |
| 171 | + |
| 172 | +### Timer Capture Flow |
| 173 | + |
| 174 | +``` |
| 175 | +User starts timer |
| 176 | + → AppViewModel.startTimer() |
| 177 | + → Task loop with configurable interval |
| 178 | + → captureTimerPhoto() [immediate + repeated] |
| 179 | + → CameraService.capturePhoto() |
| 180 | + → Upload runs in background Task |
| 181 | + → UploadHistory tracks results |
| 182 | +``` |
| 183 | + |
| 184 | +## State Management |
| 185 | + |
| 186 | +The app uses SwiftUI's native state management: |
| 187 | + |
| 188 | +- **@StateObject**: App-level ViewModel instance (in App struct) |
| 189 | +- **@ObservedObject**: ViewModel reference in views |
| 190 | +- **@Published**: Observable state properties in ViewModel |
| 191 | +- **@State**: View-local UI state (sheets, selections) |
| 192 | + |
| 193 | +### Key State Properties |
| 194 | + |
| 195 | +| Property | Purpose | |
| 196 | +|----------|---------| |
| 197 | +| `showingConfigSheet` | Controls config sheet/dialog presentation | |
| 198 | +| `currentConfig` | Active upload target configuration | |
| 199 | +| `uploadStatus` | Current upload state (idle/capturing/uploading/success/failure) | |
| 200 | +| `isCameraReady` | Camera session prepared and running | |
| 201 | +| `isTimerActive` | Auto-capture timer running | |
| 202 | +| `uploadHistory` | Rolling history of upload attempts | |
| 203 | + |
| 204 | +## Persistence |
| 205 | + |
| 206 | +### ConfigStore |
| 207 | + |
| 208 | +Persists request configurations to UserDefaults as JSON. Implements: |
| 209 | + |
| 210 | +- **Upsert**: Add new or move existing config to front |
| 211 | +- **Deduplication**: Matches by verb + URL + note |
| 212 | +- **Auto-persist**: Saves on every mutation |
| 213 | + |
| 214 | +### Upload History |
| 215 | + |
| 216 | +In-memory rolling buffer (max 100 records) tracking: |
| 217 | + |
| 218 | +- Success/failure status |
| 219 | +- Request/response summaries |
| 220 | +- Timestamp and capture number |
| 221 | +- Manual vs timer capture flag |
| 222 | + |
| 223 | +## Error Handling |
| 224 | + |
| 225 | +Errors are modeled as structured types: |
| 226 | + |
| 227 | +- **CameraError**: Permission denied, configuration failed, no camera found, capture failed |
| 228 | +- **UploadErrorReport**: Network errors, HTTP errors, includes request/response summaries for debugging |
| 229 | + |
| 230 | +Errors flow through the delegate pattern (camera) or are thrown and caught (upload), then surfaced to the UI via `uploadStatus`. |
| 231 | + |
| 232 | +## Testing Strategy |
| 233 | + |
| 234 | +### Shared Package Tests |
| 235 | + |
| 236 | +Located in `shared/Tests/`, these test: |
| 237 | + |
| 238 | +- ConfigStore persistence and deduplication |
| 239 | +- TimerConfig value clamping |
| 240 | +- UploadService request building and error handling |
| 241 | +- UploadHistory record management |
| 242 | + |
| 243 | +### Platform Tests |
| 244 | + |
| 245 | +Each platform has a `camera2urlTests` target that imports the shared package and can test platform-specific behavior. |
| 246 | + |
| 247 | +### Running Tests |
| 248 | + |
| 249 | +```bash |
| 250 | +# Shared package tests |
| 251 | +cd shared && swift test |
| 252 | + |
| 253 | +# iOS tests |
| 254 | +cd ios && make test |
| 255 | + |
| 256 | +# macOS tests |
| 257 | +cd macos && make test |
| 258 | +``` |
| 259 | + |
| 260 | +## Build System |
| 261 | + |
| 262 | +Each platform uses: |
| 263 | + |
| 264 | +- **Xcode project** with the shared package as a local dependency |
| 265 | +- **Makefile** for common operations (`make build`, `make test`, `make ui-test`) |
| 266 | +- **File system synchronized groups** for automatic source file inclusion |
| 267 | + |
| 268 | +### Adding the Shared Package |
| 269 | + |
| 270 | +Both Xcode projects reference the shared package via local path (`../shared`). The package is automatically resolved and linked. |
| 271 | + |
| 272 | +## Future Considerations |
| 273 | + |
| 274 | +When extending the codebase: |
| 275 | + |
| 276 | +1. **New shared logic**: Add to the shared package under appropriate directory |
| 277 | +2. **New platform-specific feature**: Implement in each platform app |
| 278 | +3. **New camera capability**: Extend CameraServiceProtocol and both implementations |
| 279 | +4. **New model**: Add to shared/Models with public access modifiers |
| 280 | +5. **New UI**: Implement separately per platform, sharing ViewModel interactions |
0 commit comments