Skip to content

AKaLee-IK27/evidence-capture-lite

Repository files navigation

Evidence Capture Lite

CI  Platform  Swift

A local-first, native iOS app (Swift / SwiftUI) for creating cases and attaching evidence-like media — photo, audio, and video — with metadata, a checksum, a simulated upload queue, and live device/connectivity status.

It is a portfolio demo, not a production evidence-management system: there is no backend, no authentication, and no real upload. Everything stays on the device.

Built to demonstrate the iOS skills relevant to media-capture / evidence apps: SwiftUI, SwiftData, AVFoundation, PhotosUI, FileManager, the Network framework, CoreBluetooth, privacy permissions, and async state management.

Demo

Core flow: browse a case, inspect evidence with its checksum, queue a simulated upload, go offline, and retry — recorded on the iOS Simulator

The full core flow on the iOS Simulator — browse a case → inspect evidence (real photo + checksum) → simulated upload queue → toggle offline → retry. No device required.

Features

  • Cases — create, edit, and browse cases; each groups its evidence.
  • Capture & import — take a photo (camera on device; photo picker on the Simulator), record an audio note, or import a photo/video from the library.
  • Local storage — media bytes are written to the app sandbox with a real file size and a content-addressed SHA-256 checksum; metadata lives in SwiftData.
  • Evidence detail — real media preview (image / video player / audio playback), editable notes and tags, checksum, file export via the share sheet, and a delete that removes both the record and its sandbox file.
  • Simulated upload queuequeued → uploading → uploaded/failed with progress, retry, and offline behavior. "Simulate offline" and "Simulate failure" toggles drive the offline and retry demos deterministically. Nothing leaves the device.
  • Device status — live network path (Network framework) and Bluetooth state (CoreBluetooth). A "Simulate offline" toggle drives the queue's offline demo.

Architecture

Layered and local-first, with a deliberate split between metadata and binary media:

SwiftUI screens
   │
   ▼
@Observable view state  ·  @Query (SwiftData)
   │
   ▼
Services ─────────────────────────────────────────────┐
   ├─ FileStorageService      save/load/delete + SHA-256│
   ├─ EvidenceImporter        bytes → sandbox + record  │
   ├─ EvidenceDeleter         record + file (in sync)   │
   ├─ CameraService / AudioRecorderService              │
   ├─ UploadQueueService ← SimulatedUpload (pure FSM)   │
   ├─ NetworkMonitor (NWPathMonitor + simulateOffline)  │
   └─ BluetoothStatusService (lazy CBCentralManager)    │
   │                                                    │
   ▼                                                    ▼
SwiftData (CaseRecord, EvidenceItem, UploadJob)   app-sandbox files
  • Persistence is split. SwiftData stores metadata; the media bytes live as files in the app sandbox. Deleting an item removes both — EvidenceDeleter is the single seam that keeps them in sync (and guards against wiping the storage directory).
  • Uploads are simulated. SimulatedUpload is a pure, time-free state machine (fully unit-tested); UploadQueueService ticks it on a timer and mirrors the state onto the live records. NetworkMonitor supplies real connectivity AND-ed with a simulateOffline override — one signal, not two code paths.
  • No backend, by design. Connectivity and Bluetooth are read for real, but no device integration or network upload is performed.

For the reasoning behind these choices, the tradeoffs, and what a production version would need, see docs/ARCHITECTURE.md.

Screens

Screen What it shows
Dashboard Case / evidence / pending / failed counts + live network status
Cases All cases with status and evidence count; create / edit
Case Detail Case metadata, evidence timeline, the "Add evidence" menu
Evidence Detail Media preview, metadata, checksum, notes/tags, export, delete
Uploads Simulated queue, progress, retry, offline + failure toggles, banner
Device Live network path + connection type, Bluetooth state

Screenshots live in docs/screenshots/. They are reproducible — ScreenshotTests skips in the normal test loop and regenerates on demand:

xcodebuild test -scheme EvidenceCaptureLite \
  -destination 'platform=iOS Simulator,name=iPhone 17' \
  -only-testing:EvidenceCaptureLiteUITests/ScreenshotTests \
  OTHER_SWIFT_FLAGS='-DSCREENSHOTS' -resultBundlePath /tmp/shots.xcresult
xcrun xcresulttool export attachments --path /tmp/shots.xcresult --output-path docs/screenshots

Interview relevance

iOS signal Where it shows up
SwiftUI + state Every screen; @Query-driven lists, observable services
SwiftData CaseRecord / EvidenceItem / UploadJob, cascade delete, persistence test
AVFoundation CameraService (photo), AudioRecorderService / AudioPreviewPlayer
PhotosUI Photo/video import via PhotosPicker
FileManager FileStorageService (sandbox files, SHA-256), delete + export
Networking Simulated upload queue, retry, offline; NWPathMonitor
CoreBluetooth BluetoothStatusService (lazy, permission-safe)
Privacy Explicit usage strings, local-only storage, no backend
Testing Pure state-machine tests + XCUITests that tap the real app

Requirements

  • Xcode 16+, iOS 17+ (SwiftData).
  • XcodeGen (brew install xcodegen) — the .xcodeproj is generated from project.yml.

Build, run & test

git clone https://github.com/AKaLee-IK27/evidence-capture-lite.git
cd evidence-capture-lite
xcodegen generate   # generate the .xcodeproj from project.yml
./init.sh           # build (generic simulator) + test (iPhone 17 simulator)

./init.sh regenerates the project automatically if the .xcodeproj is missing.

Override the test simulator if needed:

DESTINATION='platform=iOS Simulator,name=iPhone 15' ./init.sh

project.yml is canonical; after adding/removing files run xcodegen generate, then ./init.sh. The raw test command:

xcodebuild test -scheme EvidenceCaptureLite \
  -destination 'platform=iOS Simulator,name=iPhone 17'

Tests

24 tests (15 unit + 9 UI):

  • Unit — file storage (save/checksum/round-trip/delete), evidence import, evidence delete (record and file; fileless item is safe), tag parsing, the SimulatedUpload state machine (happy path, offline pause/resume, fail→retry), and the SwiftData models (round-trip, cascade delete).
  • UI (XCUITest) — real taps through navigation, create→relaunch persistence, note editing, the add-evidence menu, photo-preview rendering from real bytes, delete-from-detail, the offline toggle + banner, and the Device Status screen.

Runs on the Simulator

The app is fully exercisable on the iOS Simulator. Two things behave differently because the Simulator lacks the hardware, and both are handled honestly:

  • Camera — the Simulator has no camera, so "Take photo" falls back to the photo picker (a runtime AVCaptureDevice check; live capture still runs on a device).
  • Bluetooth — CoreBluetooth reports Unsupported (Simulator); the Device Status screen displays that state rather than faking a connection.

A real-device pass (live camera, real Bluetooth, and a demo video) is the only remaining step that hardware requires.

Privacy & safety

  • All media is stored locally in the app sandbox. Nothing is uploaded.
  • Usage strings explain camera, microphone, photo-library, and Bluetooth access.
  • Use only non-sensitive demo content in screenshots and any demo video.

Project status

Milestone Status
1 — Project shell & navigation
2 — Case & evidence metadata (SwiftData)
3 — Media capture & local storage
4 — Evidence detail & file actions
5 — Upload queue & device status
6 — Portfolio finish (README, screenshots, tests) ✅ (demo video pending a device)

See docs/PLAN.md for the full spec and progress.md for current state.

About

Local-first native iOS (SwiftUI) evidence-capture demo: media capture, SwiftData, a simulated upload queue, and live device status. Portfolio project — no backend.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors