Inspiration

Every week I'd walk through grocery store aisles wondering if the price on the shelf was actually a good deal, or if the same item was sitting cheaper two blocks away. I wanted a way to scan something, see what nearby stores charge for it, and make a smarter decision on the spot — without opening five browser tabs or hunting for coupons. That everyday frustration became PriceRadar.

What it does

PriceRadar is an iOS app that lets you scan a product barcode (or search by name) and instantly compare prices at stores near you. The core loop is simple:

  1. Scan or search a product
  2. See nearby stores and what they charge, sorted by price or distance
  3. Tap a store to get directions or report the price you actually see on the shelf
  4. Earn points for contributing prices that help everyone else save money

Under the hood it combines:

  • AVFoundation + Vision for barcode scanning
  • MapKit + CoreLocation to discover real stores around you in real time
  • Open Food Facts API for product info and images on millions of items
  • Firebase Firestore for crowd-sourced, community-verified pricing
  • SwiftUI + MVVM architecture throughout

How we built it

I started with the scanner — getting a camera preview working reliably in SwiftUI was the first real challenge. From there I layered on the architecture one piece at a time:

Phase 1 — Core scanning and local data Built the barcode scanner with AVFoundation and Vision, wired it to a local JSON database of products, stores, and prices. This proved out the concept quickly without needing any external dependencies.

Phase 2 — Real store discovery Replaced the static store list with MapKit Local Search so the app finds actual stores near the user's location rather than hardcoded ones. Integrated CoreLocation with battery optimizations (reduced accuracy, auto-stop after first fix).

Phase 3 — Live product data Integrated Open Food Facts as the primary product lookup and Barcode Monster as a fallback. This opened the catalog from ~20 hardcoded items to millions of real products.

Phase 4 — Community pricing Added Firebase Firestore for crowd-sourced price submissions. Users can now report a price at any store, and those prices surface for everyone else scanning the same barcode. Added anonymous authentication, a points/gamification system, and receipt scanning via Vision's text recognition.

Phase 5 — Security & polish Moved all API keys out of the plist and source files into a .env file loaded at runtime, so secrets are never committed to git.

Challenges we ran into

Camera black screen on first launch The preview layer wasn't connecting properly when the AVCaptureSession was created inside a SwiftUI lifecycle. The fix was a custom PreviewView: UIView subclass that overrides layerClass to return AVCaptureVideoPreviewLayer.self, ensuring the session is set before the view ever renders.

Device overheating Within the first real-device test it was clear the Vision framework was being hammered. Every frame was creating a new VNDetectBarcodesRequest, and at 30 fps that's 30 new objects per second with no reuse. Storing the request as a lazy property and throttling frame processing to 0.5-second intervals solved both the heat and the battery drain.

Store results were geographically wrong MapKit's radius search was returning a diameter (region spanning radius * 2 on each side) instead of an actual radius from the user's point. Stores up to double the intended distance were appearing. Correcting the CLLocationDistance calculation fixed the results.

API key in version control The GoogleService-Info.plist was committed with the real Firebase API key. The solution was to load all Firebase config from a .env file at runtime using FirebaseApp.configure(options:), redact the plist, and remove the .env from the git index with git rm --cached. The key itself still needed to be rotated since it existed in prior commit history.

Crowd-sourced data bootstrapping A price comparison app is only useful when it has prices. Until enough users contribute data, most stores show "Price unavailable." The points/gamification system and the "Report Price Here" button directly on each store's detail sheet are designed to lower the friction on submissions as much as possible, turning every scan into a potential contribution.

Accomplishments that we're proud of

Building a full barcode-to-price pipeline from scratch. Getting the entire flow working end-to-end — scan a barcode, identify the product, find real stores nearby, fetch community prices, and display it all in a clean UI — felt like a real milestone. Each layer (Vision, MapKit, Open Food Facts, Firebase) had to connect cleanly to the next, and seeing it work on a real device for the first time was genuinely satisfying.

Receipt scanning. Adding OCR-based receipt scanning so users can photograph an entire grocery receipt and submit all those prices in one batch was a meaningful leap beyond the single-barcode flow. It turns a single user interaction into potentially 20 price data points at once.

Making the app work without data. The graceful degradation stack — Firebase → Open Food Facts → Barcode Monster → local JSON — means the app is always useful even when individual services are down or a product isn't in any database yet. That kind of resilience took deliberate effort to architect and test.

What we learned

Swift concurrency is unforgiving about actors. The biggest debugging session of the project was tracking down why search results would arrive (confirmed in logs) but never show on screen. The root cause: @Published property assignments were happening off the @MainActor because a bare Task { } inside a @MainActor class doesn't inherit the actor context. Adding Task { @MainActor in } fixed it instantly. A small detail with a huge visible impact.

Third-party APIs lie in their documentation. The Open Food Facts v2 endpoint (/api/v2/search?search_terms=) claims to support text search but effectively ignores the parameter and returns the entire 4-million-product database in default order — which is why searching "crackers" returned French mineral water. Switching to the legacy cgi/search.pl endpoint with sort_by=unique_scans_n gave genuinely relevant results.

String matching needs explicit boundaries. The store-relevance system matched product categories to search terms using partial string containment. That sounds sensible until "Appetizers" matches the key "pet" and MapKit starts returning PetSmart for a crackers search. The fix was to split the comma-separated OFF category string into individual tokens before matching, and sort keys longest-first so specific terms win over short ones.

MapKit is a capable but opinionated search engine. You can't just pass an arbitrary category string as a naturalLanguageQuery — you have to give it terms that correspond to real place types. Mapping product categories to meaningful store search terms (and keeping that mapping well-maintained) turned out to be a significant piece of ongoing work.

Performance problems compound fast on mobile. Processing every video frame through a new Vision request was enough to overheat a real device within minutes. Frame-dropping to 2 fps, reusing a single VNDetectBarcodesRequest instance, and reducing GPS accuracy collectively cut CPU usage from 60–80% down to 10–15%. None of those changes were individually obvious — I had to profile each one.

What's next for Price Radar

Automated price ingestion — moving beyond crowd sourcing. Relying entirely on users to report prices is the biggest limitation of the current model. The next priority is finding ways to gather prices programmatically and passively:

  • Retailer API partnerships — chains like Walmart, Target, and Kroger expose pricing through affiliate or developer APIs. Integrating even one major retailer would seed the database with millions of verified prices immediately.
  • Web scraping pipeline — a server-side service that periodically scrapes public pricing pages for major grocery chains and writes results to Firestore, so the app has baseline prices before any user submits anything.
  • Receipt OCR at scale — as more users submit receipts, the aggregated data becomes a price feed in itself. Improving matching confidence and expanding the product catalog will make each receipt submission more valuable.
  • Store loyalty card integration — partnering with loyalty programs to receive anonymized purchase data (with user consent) as a high-volume, high-accuracy pricing signal.

Price history and trend charts. Showing how a product's price has changed over time at a specific store — and alerting users when it drops below a threshold they set — transforms PriceRadar from a point-in-time lookup into a genuine savings tool.

Shopping list with live price totals. Build a list, scan or search items into it, and see a running total across different store combinations — "this basket costs $47 at Walmart vs. $53 at Target." Route optimization could then suggest which store to hit first.

Personalized deal alerts. Push notifications when a product on your list drops in price nearby, powered by the price ingestion pipeline above.

Built With

Share this project:

Updates