DEV Community

Oleksandr Prudnikov
Oleksandr Prudnikov

Posted on

I built an offline-first iOS app in 43 days with Swift, SQLite, and Claude Code

My wife buys secondhand items at flea markets and car boot sales in the UK and resells them on eBay and Vinted. She was tracking everything in Google Sheets and kept forgetting what she paid for things. When someone sent an offer she had no idea if it was a good deal.

So I built her an iOS app. That was 43 days ago. It's now on the App Store, completely free, and a few people are actually using it. Here's how I built it and the technical decisions along the way.

What the app does

You take a photo at the market, enter the price, and the app tracks the item through its lifecycle: purchased → listed on platforms → sold. It calculates profit after all expenses — entry fees, transport, packaging. Supports 4 currencies with automatic exchange rates. Works completely offline.

Tech stack

  • Swift + SwiftUI — iOS 17+
  • SQLite via GRDB — local storage with WAL mode, foreign keys, composite indexes
  • Google Drive API — optional photo sync + CSV export (drive.file scope only)
  • BGTaskScheduler — background photo uploads
  • CoreLocation — auto-suggest nearby markets
  • No backend — everything runs on device, zero server costs

The architecture

SwiftUI Views
    ↓
ViewModels (ObservableObject)
    ↓
Services (GoogleAuth, Export, Location, Notification)
    ↓
DatabaseStore (Swift Actor — all CRUD operations)
    ↓
SQLite (GRDB, WAL mode) + Local Images
Enter fullscreen mode Exit fullscreen mode

The key decision was making it offline-first. Car boot sales happen in fields with no signal. The app can't depend on network for anything core. Google Drive sync is purely optional — you can use the app forever without ever connecting a Google account.

Day 1–7: JSON files and regret

I started with the simplest possible storage — JSON files in the Documents directory. One file per entity type (items, markets, sellers, expenses). Read the whole file into memory, modify, write it back.

This worked fine for 20 items. It did not work fine for 200 items. Loading the entire items array into memory on every read was getting slow, and there was no way to do efficient queries like "show me all unsold items sorted by date, paginated."

Day 18: The SQLite migration

I migrated everything to GRDB (a Swift SQLite wrapper). This was the most significant single-day change in the project.

GRDB gives you:

  • Typed record structs that map to tables
  • A migration system (versioned schema changes)
  • WAL mode for concurrent reads
  • Proper indexes for fast queries

My migration system has 4 versions so far:

// v1: Initial schema — all tables, indexes, foreign keys
// v2: Composite indexes for paginated list queries
// v3: Crockford Base32 SKU regeneration
// v5: Market "askForEntryFee" boolean column
Enter fullscreen mode Exit fullscreen mode

The paginated queries went from "load everything into memory and filter" to actual SQL with LIMIT/OFFSET on indexed columns. Night and day difference.

Offline-first with optional Google sync

The sync model is simple. Items default to "local" status. If you connect Google Drive:

  1. Photos queue for upload via SyncQueueService
  2. BGTaskScheduler processes the queue even when app is suspended
  3. CSV export runs on a 5-minute timer — compares MAX(updatedAt) vs last sync timestamp, only uploads if data changed
  4. CSV is gzip-compressed before upload
  5. Google auto-converts the CSV to a Google Sheet (no Sheets API needed)

I deliberately avoided the Sheets API. Using only drive.file scope means the app can only access files it created itself. Better privacy, simpler OAuth, fewer permissions to explain to users.

Multi-currency with exchange rates

My wife buys in GBP at UK markets but sometimes sells in EUR or USD on eBay. The app needs to show profit in your home currency regardless of what currencies you bought and sold in.

I fetch daily rates from a free API and cache 365 days of history locally in SQLite. For conversions I support three paths:

  • Direct rate — GBP → USD
  • Reverse rate — USD → GBP (1/rate)
  • Cross rate via common base — EUR → CAD via USD

If you're offline for days the app uses cached rates and shows a warning that they might be stale. You can refresh manually when you get signal.

SKU generation

Every item gets a unique SKU like FH-20D6G0NR. I use Crockford Base32 encoding — uppercase only, no ambiguous characters (I, L, O, U are excluded). The SKU is time-based with an atomic counter for guaranteed uniqueness.

Users can copy the SKU and paste it into their eBay/Vinted listing descriptions, then match up sales later.

Building with Claude Code

I used Claude Code throughout the entire build. It's genuinely how I shipped this in 43 days as a solo developer. The AI handled a lot of the boilerplate — GRDB record types, SwiftUI form layouts, CSV generation, the Google OAuth flow.

Where it helped most was the migration from JSON to SQLite. I could describe the existing data model and the target schema and Claude Code would generate the migration code, the new record types, and update all the queries.

Where it didn't help: design decisions. What should be offline vs synced, how to handle currency conversion edge cases, which features actually matter for my wife's workflow. Those are product decisions that need human context.

The Figma redesign (Day 23)

I'm not a designer but I knew the app looked like a developer built it. So I created a proper design system in Figma first:

  • Font: Roboto Flex (variable weight)
  • Design tokens: gold #F9DDA5, badge #BBCBF7, card bg #F8FAFB, accent #334B90
  • Neumorphic shadows: two styles — large (radius 30) and small (radius 15)
  • Light mode only: explicit colors everywhere, no system adaptive

Then rebuilt every screen to match the Figma exactly. The 1:1 Figma px → iOS pt mapping made this surprisingly straightforward.

What I'd do differently

Start with SQLite from day one. The JSON-to-SQLite migration was worth it but I lost time building something I knew I'd throw away. If your app will have more than ~50 records, just use a database from the start.

Add search earlier. Users asked for it immediately. Being able to find an item by title or SKU across all tabs seems obvious in hindsight.

Don't overthink Google integration. I spent too long trying to make the sync perfect. The simpler approach — CSV upload with change detection — works well and is much easier to maintain than real-time sync.

Current state

  • ~18,100 lines of Swift across 69 source files
  • 16 selling platforms supported
  • 4 currencies with daily exchange rates
  • 4 database migrations
  • 8-page onboarding walkthrough
  • Free on the App Store, no ads

The whole thing runs with zero server costs. GitHub Pages for the website, GitHub Actions for CI/CD, Apple Developer account for distribution. That's it.

If you're curious:

Happy to answer questions about the architecture, the GRDB setup, or the Claude Code workflow.

Top comments (0)