TankIt Role · Solo Platform · iOS 18+ Stack · Swift 6 / SwiftUI Year · 2026
Behind the build

A fuel tracker that never phones home.

TankIt scans receipts, finds the cheapest pump, and crunches your fuel statistics — and runs every piece of that intelligence on-device. The only network calls go to two government price registries. Here’s how that’s built.

Privacy by design Apple Vision OCR Foundation Models Swift 6 strict concurrency SwiftData WidgetKit

01Overview

TankIt is a personal fuel management app for drivers in Austria and France. The core loop is simple: snap a photo of a fuel receipt, let the app read it automatically, and watch your history, efficiency, and spending build up over time.

On top of that, a real-time price finder surfaces the cheapest station near you, and home screen widgets bring key numbers to a glance.

A privacy-first iOS app that turns fuel receipts into spending insights, finds the cheapest nearby station in real time, and stays out of the cloud.
0
third-party SDKs
100%
on-device ML
2
countries supported
4
widget variants

02Privacy

Privacy isn’t a feature, it’s a constraint that shapes every other decision. Concretely:

  • All ML inference (OCR, rectangle detection, LLM extraction) runs entirely on-device.
  • No analytics SDK, no crash reporter that phones home, no user accounts.
  • Receipt images are stored locally on your iPhone, never transmitted.
  • The only network calls leave the device for the two government price APIs — and only your GPS coordinate goes with them.
  • You can export the entire dataset (CSV + ZIP of receipts) at any time. Nothing to lock in.
We physically can’t see your data because it never leaves your phone.

03On-device intelligence

OCR — Apple Vision framework

VNRecognizeTextRequest runs at .accurate level, with language correction tuned per use case:

  • Receipt text → language correction enabled, languages ["fr-FR", "de-AT", "en-US"].
  • Odometer photos → language correction disabled so digit sequences aren’t mangled into words.

Structured extraction — LLM + regex pipeline

After OCR produces raw text, ExtractionService runs a two-stage pipeline:

  • LLM path (Foundation Models, when available): Apple’s FoundationModels framework (LanguageModelSession) with a @Generable structured output type — the on-device model returns typed fields directly, no string parsing. @Guide annotations steer each field toward the expected format.
  • Regex fallback: A hand-crafted multilingual parser for receipts from French, Austrian, and German stations — covering labelled patterns (Prix/L, Preis/L, GESAMTSUMME, NET À PAYER) and free-form layouts. A derived-value step closes the price triangle: if any one of {total, volume, price/L} is missing but the other two are present, it calculates the third.
step 01
Camera
Capture receipt photo
step 02
Vision OCR
Local text recognition
step 03
On-device LLM
@Generable typed fields
step 04
SwiftData
Local persistence only
Nothing leaves the device. No API call, no cloud function — the entire extraction runs locally in under a second.

Receipt image correction

ReceiptCorrectionView wraps VNDetectRectanglesRequest to find the receipt boundary, then applies a perspective warp. Available both immediately after scanning and retrospectively from the fill history.

04External APIs

The only outbound traffic from TankIt: two official government fuel price registries. No third-party data brokers, no scraping.

AT · Austria

e-control.at

Federal energy regulator’s public price API. Internal fuel codes (E95, Diesel, LPG) map to SUP, DIE, GAS.

api.e-control.at/sprit/1.0/search/gas-stations/by-address
FR · France

data.economie.gouv.fr

French government’s open fuel-price dataset, queried via ODS v2.1 spatial filter — up to 20 stations within 5 km in one request.

data.economie.gouv.fr/api/explore/v2.1/…/prix-des-carburants-en-france-flux-instantane-v2

Routing — FuelPriceRouter

A shared router abstracts both APIs behind a single fetch(coordinate:countryCode:fuelType:) call. It:

  • Auto-detects the relevant country from GPS coordinates.
  • Caches results for 10 minutes with a ~1 km grid quantisation key.
  • Persists the cheapest station to the App Group UserDefaults so widgets always show fresh data without their own network call.
  • Triggers a WidgetCenter.shared.reloadTimelines after each successful fetch.

Apple Maps — navigation deep-link

The price finder uses maps://?daddr=lat,lon&dirflg=d to open turn-by-turn navigation with zero additional SDK dependencies.

05Tech stack

Language & UI

  • Swift 6 — strict concurrency throughout (@MainActor, Sendable, structured tasks).
  • SwiftUI — 100% declarative UI; the only UIKit view controller is the camera viewfinder.
  • WidgetKit — four widget types (small + medium for both price finder and efficiency).

Persistence

  • SwiftData — model layer (FuelEntry, Vehicle) with @Model annotations, relationship rules, and reverse-index queries.
  • UserDefaults (App Group) — shared container group.com.tankit.app for passing live data between the main app and widgets without a server round-trip.

Intelligence

  • Apple Vision — on-device OCR & rectangle detection.
  • FoundationModels — on-device LLM for structured field extraction, with a regex fallback when the framework isn’t available.
  • StoreKit 2 — modern transaction-listener subscription model.

06Architecture highlights

01 · Privacy by design

Everything stays on device

All ML inference (OCR, rectangle detection, LLM extraction) runs entirely on-device. No analytics SDK, no crash reporter that phones home, no user accounts.

02 · StoreKit 2

Subscriptions, the modern way

SubscriptionService uses Product.products(for:), Transaction.currentEntitlements, and a Transaction.updates listener for real-time entitlement changes.

03 · Math correctness

Partial-fill efficiency

A common bug in fuel trackers: partial fills break L/100 km calculations. efficiencyPairs(from:) accumulates volumes across consecutive partial fills and only closes the cycle at the next full fill.

04 · ZIP export

No third-party archiver

ExportService ships a hand-written ZIP builder (STORED method, no compression) in ~80 lines of pure Swift. JPEGs are already compressed — STORED keeps it dependency-free.

05 · App Group bridge

Widgets stay fresh

The router persists the cheapest station to the App Group UserDefaults, then calls WidgetCenter.shared.reloadTimelines. Widgets render fresh data without making their own network request.

06 · Caching

Quiet on the network

Price queries are cached for 10 minutes against a quantised ~1 km grid key. Same neighbourhood, same fuel, no duplicate request.

07Platform & requirements

  • iOS 18+, iPhone only. On-device LLM extraction kicks in where Foundation Models is available; a regex parser handles the rest.
  • Xcode 16 / Swift 6.
  • Location permission for price finder (optional — manual address search available).
  • Camera permission for receipt scanning (optional — manual entry available).