/* Route-Critical (index, V6 §3.6.3 — erzeugt von tools/generate_critical_css.mjs) */ .collapse:not(.show){display:none}.navbar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar>.container-fluid{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.mydraft-svg-icon{display:inline-block;vertical-align:middle;color:currentcolor;flex-shrink:0;transition:color var(--duration-fast, 150ms) ease,stroke var(--duration-fast, 150ms) ease,fill var(--duration-fast, 150ms) ease}.mydraft-svg-icon circle,.mydraft-svg-icon line,.mydraft-svg-icon path,.mydraft-svg-icon polygon,.mydraft-svg-icon polyline,.mydraft-svg-icon rect{transition:stroke var(--duration-fast, 150ms) ease,fill var(--duration-fast, 150ms) ease}@layer components{.live-pulse-dot{width:8px;height:8px}}.min-w-0{min-width:0}@layer overrides{.live-radar-header-badge{background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.35);color:#fca5a5;font-size:.75rem;font-weight:700;transition:.25s;cursor:pointer}}@layer overrides{.live-pulse-dot{width:8px;height:8px;background-color:var(--threat-critical,#ef4444);border-radius:50%;display:inline-block;box-shadow:rgba(239,68,68,.7) 0 0 0 0}}@layer overrides{body.light .live-radar-header-badge,html[data-bs-theme=light] .live-radar-header-badge{background:#fbd9dd;border-color:#eeb2b8;color:#8a1723!important}}.isharestuff-live-ticker{color:var(--color-text-body,#f8fafc);backdrop-filter:none;flex-wrap:wrap;row-gap:.3rem;background:var(--color-surface,rgba(15,23,42,.85))!important;border:1px solid var(--color-border,rgba(255,255,255,.12))!important;padding:.45rem .5rem!important;margin-bottom:.75rem!important}.ticker-content{mask-image:linear-gradient(to right,transparent 0px,rgb(0,0,0) 14px,rgb(0,0,0) calc(100% - 14px),transparent 100%);touch-action:pan-y;cursor:grab;user-select:none;-webkit-tap-highlight-color:transparent;order:1;width:100%;max-width:100%;flex:0 0 100%!important}.isharestuff-live-ticker .ticker-lead-group{order:2;width:100%;justify-content:space-between;margin-right:0!important}.ticker-track{display:flex;align-items:center;white-space:nowrap;will-change:transform;transform:translate3d(0,0,0);--ticker-speed:1;animation-name:isharestuffTicker;animation-timing-function:linear;animation-iteration-count:infinite;animation-duration:calc(26s / var(--ticker-speed, 1))}.ticker-link{max-width:min(78vw,340px);font-size:.82rem;display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:baseline;transition:color .2s;color:var(--color-text-heading,#f8fafc)!important}.isharestuff-live-ticker .ticker-ctrl-btn{min-width:44px;height:44px;font-size:.78rem;-webkit-tap-highlight-color:transparent}.ticker-controls-bar{gap:.4rem}.ticker-counter{font-family:var(--bs-font-monospace, ui-monospace, monospace);font-size:.72rem;line-height:1;color:var(--color-text-secondary,#94a3b8);min-width:3.4em;text-align:center;user-select:none;white-space:nowrap;margin-left:auto}.ticker-item{font-size:.82rem}.ticker-item .ticker-date{font-size:.72rem;vertical-align:baseline}.ticker-text-group{display:inline-flex;align-items:baseline}.ticker-badge-live-btn{border:0;cursor:pointer;text-decoration:none!important}.ticker-badge{font-size:.78rem!important;min-height:44px!important;min-width:44px!important;padding:6px 12px!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.signal-live-strip{display:flex;align-items:center;gap:.625rem;min-height:44px;margin-bottom:1rem;padding:.4rem .875rem;border-radius:9999px;border:1px solid rgba(var(--color-border-rgb,148,163,184),.35);background:rgba(var(--color-surface-rgb,15,23,42),.6);backdrop-filter:blur(12px) saturate(140%);overflow:hidden}.signal-live-badge{display:inline-flex;align-items:center;gap:.35rem;flex:0 0 auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#fda4af}[data-bs-theme=light] .signal-live-badge,body.light .signal-live-badge{color:#be123c}.signal-live-dot{width:8px;height:8px;border-radius:50%;background:#f43f5e;animation:1.6s ease-in-out 0s infinite normal none running signalLivePulse}.signal-live-strip.is-online .signal-live-dot{background:#22c55e;animation:auto ease 0s 1 normal none running none}.signal-live-item{font-size:.82rem;line-height:1.35;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.menue-widget .nav-tree-link{min-width:44px;display:flex;align-items:center;word-break:break-word;overflow-wrap:anywhere;max-width:100%;min-height:44px!important}.menue-widget .nav-title-text{overflow-wrap:anywhere;word-break:break-word}.menue-widget .menue-widget-btn{min-height:44px;min-width:44px;display:inline-flex;align-items:center;justify-content:center}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-toggler{display:none}}@media (prefers-reduced-motion:reduce){@layer overrides{#news-popup,#news-toast-banner,.animate-pulse,.live-pulse-dot,.live-pulsing-dot,.news-item-card{animation:auto ease 0s 1 normal none running none;transition:none}}.ticker-track{animation:auto ease 0s 1 normal none running none!important}.ticker-content{cursor:default}.signal-live-dot{animation:auto ease 0s 1 normal none running none}}@media (min-width:768px){.isharestuff-live-ticker{flex-wrap:nowrap;row-gap:0;backdrop-filter:blur(12px);padding:.5rem!important;margin-bottom:1rem!important}.isharestuff-live-ticker .ticker-content{order:0;width:auto;max-width:none;flex:1 1 auto!important}.isharestuff-live-ticker .ticker-lead-group{order:0;width:auto;justify-content:flex-start;margin-right:1rem!important}.ticker-controls-bar{gap:.3rem}.isharestuff-live-ticker .ticker-ctrl-btn{min-width:30px;height:auto;font-size:inherit}.ticker-counter{font-size:.66rem;margin-left:0}.ticker-track{animation-duration:calc(40s / var(--ticker-speed, 1))}.ticker-link{max-width:340px;font-size:.85rem}.ticker-item{font-size:.85rem}.ticker-badge{font-size:inherit!important;min-height:0!important;min-width:0!important;padding:.5rem 1rem!important}}@media (max-width:359.98px){.ticker-counter{display:none}.ticker-controls-bar{gap:.3rem}.ticker-badge{padding:6px 9px!important}}@media (max-width:767.98px){.signal-live-strip{flex-wrap:wrap;align-items:flex-start;border-radius:1rem;padding:.5rem .75rem;row-gap:.25rem}.signal-live-badge{margin-top:.1rem}.signal-live-item{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;text-overflow:clip;overflow-wrap:anywhere;line-height:1.3}} 🔒 I built an offline-first iOS app in 43 days with Swift, SQLite, and Claude Code | Sichere Programmierung – TSECURITY.DE Zum Hauptinhalt springen
•••••
Sichere ProgrammierungTIL: What the User Count Option Really Affects in GBase Database(18.10.2026 um 05:46 Uhr)
••
Sichere ProgrammierungScopeTrace — find the clause, then question the promise(18.10.2026 um 05:48 Uhr)
••••••••
Sichere ProgrammierungTIL: What the User Count Option Really Affects in GBase Database(18.10.2026 um 05:46 Uhr)
••
Sichere ProgrammierungScopeTrace — find the clause, then question the promise(18.10.2026 um 05:48 Uhr)
•••
Intelligence View
⚡ tsecurity.de Intelligence

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…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!

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






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






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.

🔍 CTI & Forensik

Cyber Threat Intelligence & Forensik

ATT&CK-Navigator · IoC-Radar · Exploit-Belege
CTI Threat Relationship Graph
Akteure · Techniken · Beziehungen
4 Knoten · 3 Relationen
CVE / Incident Threat Actor Software MITRE ATT&CK CWE Weakness IoC
Intelligence Digest — kostenlos Täglich die wichtigsten Security-News · jederzeit abbestellbar
Community Rating & Social Proof
⭐ Leser-Wertung
–
Ø / 5.0
Noch keine Leser-Bewertungen
War diese Seite hilfreich?
0
Jetzt Bewertung abgeben
🟢 Niedrig (1-3.9) 🟡 Mittel (4.0-6.9) 🔴 Hoch (7.0-8.9) Kritisch (9.0-10.0)

Verwandte Story-Cluster & Quellen (Vektor-KI)

2 Quellen
TIL: GBase Database Default Character Set Can Cause Chinese Text Issues
1 weitere Quelle
Ähnliche Beiträge
🔍 Verwandte News

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

Thematisch verwandte Begriffe: built, offlinefirst, days, with · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
Nächster Beitrag