Concepts
Architecture
TripLibre is two things: a thin SwiftUI app that talks to iOS, and a platform-independent Swift package,
HHTCore, that does everything else. All logic that matters for data quality lives in the package and is
tested without a phone.
Data flow
- SensorsCoreLocation fixes, CLVisit events, CoreMotion activity
- Raw tablesAppend-only, never editedread-only
- InferenceStays → places → trips → mode segmentsinterchange
- DiaryVisits & trips you review and correctyou edit here
- OutputsStats, map, CSV / JSON / GeoJSON / SQLiteterminus
The collector writes every fix to SQLite immediately. Inference runs on launch, when the app comes to the foreground, when the collector decides you've stopped, and from a background refresh task. It is deterministic: the same raw data and the same corrections always produce the same diary. After each run (and after any edit) the app also refreshes a small summary for the Home Screen widgets.
Repository layout
App/ SwiftUI iOS app
HHTApp.swift entry point, AppDelegate, background refresh task
AppModel.swift owns the store and services; views observe it
Location/ LocationCollector, MotionSync, PlaceGeocoder
Screens/ Today, Timeline, Map, Stats, Places, Settings, detail screens
Support/ theme, formatting, demo data, Info.plist, WidgetBridge
Widgets/ WidgetKit extension: read-only Home Screen widgets
Packages/HHTCore/ Swift package, no third-party dependencies
Sources/HHTCore/
Database/ SQLite wrapper, schema + migrations, TravelStore
Inference/ StayDetector, ModeClassifier, InferenceEngine, InferenceConfig
Editing/ EditService (every correction) + audit log
Analytics/ MobilityMetrics (HHTS metrics, r_g, entropy, tours…)
Export/ Exporter, Importer, CSV
Geo/ haversine, centroids, convex hull, Douglas–Peucker, polyline
Models/ records (Place, Visit, Trip…) and enums (TravelMode…)
Widget/ WidgetSnapshot, the summary the widgets read
Tests/HHTCoreTests/ fixture scenarios, edits, reprocessing, export
analysis/ Python loader (hht.py), example analysis, export contract check
docs/ hhtdiary.org: organization pages (/, vision/, roadmap/, products/), these docs, markdown specs
project.yml XcodeGen spec — HHT.xcodeproj is generated from it
Makefile test / sim / phone / ci workflows
The two halves
App (iOS-only)
Everything that needs iOS frameworks: LocationCollector (a two-state machine, moving vs stationary, described below),
MotionSync (queries CoreMotion's ~7-day activity history when inference runs), PlaceGeocoder (optional reverse
geocoding), and the SwiftUI screens. AppModel is a single shared object that owns the
TravelStore, InferenceEngine and EditService. Views observe its revision
counter and reload after any change.
HHTCore (pure Swift)
No UIKit, no CoreLocation, no dependencies beyond Foundation and the system SQLite. It builds and tests with plain
swift test on macOS, which is what CI runs. The main types:
| Type | Responsibility |
|---|---|
TravelStore | Typed access to the SQLite database: points, motion, places, visits, trips, flights, phases, audit log. |
InferenceEngine | Incremental and range reprocessing. Orchestrates the detectors and protects user edits. |
StayDetector | Cleans fixes and finds stays (visits) with sequential clustering and gap reasoning. |
ModeClassifier | Classifies intervals, segments trips into modes, picks the main mode. |
EditService | Every user correction, each one transactional and audit-logged. |
MobilityMetrics | Survey-style metrics for any date range. |
Exporter / Importer | The export bundle; CSV, GPX and airport-list import. |
WidgetSnapshot | The small, versioned summary the Home Screen widgets read (counts, labels, times). |
Three data layers
The single most important design rule: raw observations, automatic guesses and your corrections are kept apart. That's what makes it safe to re-run inference at any time.
raw_location_points, motion_activities. Written by the collector, append-only, never edited.visits, trips, trip_segments with user_status = 'auto'. Written by inference; may be rebuilt at any time.user_status in confirmed | edited | manual. Locked against rebuilds; every change in audit_log.Background collection
The collector switches between two states to balance accuracy and battery:
| State | Location settings | Leaves when |
|---|---|---|
| moving | Continuous updates. Balanced: 10 m accuracy / 15 m filter. Precise: best / 5 m. | No fix > 60 m from an anchor for 3 min → stationary |
| stationary | Standard updates off. 150 m geofence around the stop; CLVisit and significant-change monitoring on. | Geofence exit, CLVisit departure, or a significant-change fix > 150 m away → moving |
- Region exits, visits and significant changes relaunch the app even after it was terminated;
AppDelegaterestarts collection on every launch. - On entering stationary mode the last fix is written again as a
stationary_marker, so inference can tell "quiet because stationary" from "lost signal". - A background refresh task (
com.zyang91.hht.refresh, roughly every 2 h when iOS allows) runs inference and the daily backup.
Home Screen widgets
The HHTWidgets extension (Widgets/) shows the diary on the Home Screen. It is a window onto the
app, not a second engine: it never opens the database, collects location or runs inference.
| Step | Where | What happens |
|---|---|---|
| Build | WidgetSnapshot.build(store:…) (HHTCore) | Today counts with the same overlap rules as the Today screen; 7-day totals from the same Analytics.compute as Stats; tracking state, last point, trips to check. |
| Write | WidgetBridge (App/Support) | Runs 2 s after a revision bump, a location-permission change or a collector mode change, at most every 10 min while new fixes arrive, and when the app goes to the background. Writes widget-snapshot.json atomically into the App Group group.com.zyang91.hht. |
| Reload | WidgetCenter | Asked to reload only when the content changed (or at most hourly), to stay inside iOS's reload budget. |
| Show | SnapshotProvider (Widgets) | Reads the file once per timeline and adds an entry every 15 min plus one at midnight, so "3 min ago" and the day rollover stay right without the app running. After midnight the week widget relabels itself "7 days to <day>" until a new snapshot arrives. |
- Privacy: the snapshot holds no points, routes or coordinates, and a place name only if you named the place yourself (never an address). Names on the widget are marked
.privacySensitive(). - Versioning: the file carries
version. A widget ignores a snapshot from an incompatible build and shows its set-up state until the app writes a new one. BumpWidgetSnapshot.currentVersionwhen you change the shape. - Not live: widgets update when iOS lets the app run, so they show "Updated … ago" rather than claiming live GPS. Hours without new points are normal while you stay put; the status widget calls it quiet only after 12 h.
- Taps open
hht://today,hht://stats(Stats on 7 days) orhht://settings, handled inRootView.onOpenURL. - Quick Stamp: the Stamp widget opens
hht://stamp; the app pops Today back to its root (so the confirmation and Undo are on screen), then calls the sameAppModel.stampNow()as its button, which goes throughEditService.stampOnce, so a link delivered twice or a double tap within 30 s keeps one stamp. The stamp is written in the app, not the extension: a widget can't get a fresh fix or write the database safely, and an App Intent running in the extension would need both. Before onboarding the link only opens the app. The snapshot'slastStampAtlets the widget confirm the last stamp.
What each widget looks like is on the UI guide.
Design priorities
When two goals conflict, the earlier one wins:
- Data integrity: never lose or silently change what was recorded or corrected.
- Collection reliability: keep recording in the background.
- Correct reconstruction.
- Fast correction: one or two taps.
- Exportability.
- Analytics.
- Polish.
Build and CI
There is no Docker image: iOS apps can only be built and signed on macOS with Xcode. The reproducible "package" is
project.yml (XcodeGen) + Makefile + GitHub Actions on macOS runners. CI runs the core tests,
an end-to-end check that the Python loader reads a real export, simulator and device builds, and a check that
HHT.xcodeproj matches project.yml. See Contributing.