The Little App That Almost Wasn't

It all started with a shoebox. A dusty, forgotten shoebox filled with my childhood. Pokémon cards, worn at the edges, each one a memory. I wanted to bring that magic into the digital age. I imagined an app that could look at any card, in any language, and instantly tell you its story, its rarity, its value. A true Pokédex for the real world.

For three solid weeks, that vision was my entire world. My life shrank to the glow of my monitor, fuelled by coffee and pure nostalgia. I averaged over 10 hours a day, losing myself in the complexities of VisionKit for multi-language OCR, wrestling with dynamic APIs, and carefully crafting a dark-mode UI that felt like a secret gadget a real Pokémon trainer would use. I wasn't just building an app; I was building my dream tool. Then, it was ready. And I was proud.

Then, I hit the wall. The faceless, unforgiving wall of App Store Review.

The first rejection stung. "Metadata violation." A simple fix, I thought. I scrubbed the forbidden "Pokémon" word from every field. Resubmit.

The second rejection felt personal. Now it was the screenshots. They contained images of the cards. I spent days generating Pokémon-like creatures with AI, meticulously replacing every real card in my beautiful UI previews with these strange, soulless imitations. Resubmit.

Rejections three, four, and five were a blur of minor tweaks and growing despair. Then came the final blow—the one that nearly broke me. An reviewer note explained that the issue wasn't just my metadata or screenshots anymore; it was the content inside the app. They asked me to remove the images of the cards themselves.

I was stunned. It felt like being asked to build a calculator that can't show numbers. An app to scan Pokémon cards that can't show Pokémon cards? It was a checkmate. Defeated, and honestly, a little heartbroken, I wrote back that the app would be meaningless without its core function and withdrew the submission.

The project I had poured my soul into was now a ghost on my hard drive. I felt completely demoralized. The motivation that had burned so brightly for weeks was extinguished. All that work, for nothing.

But a small, stubborn part of me refused to let it go. With a knot in my stomach, fearing the ban-hammer that swings so easily these days, I filed a formal appeal with the App Review Board. I laid out my case, professionally but passionately, then tried my best to forget about it. I even started building another, "safer" app, convinced I'd submit that one to Shipaton instead.

On September 26th, nearly a month later, an email popped up with a subject line from Apple that I almost deleted. My appeal had been accepted. The rejection was overturned.

The feeling was electric. It was a wave of adrenaline and vindication. I dropped everything, ripped out all the AI-generated placeholders, restored my original metadata, and hit the "Ready for Sale" button with more satisfaction than I've ever felt in my life.

I posted a bit about it on Reddit and Twitter, not expecting much. But then, it happened. A download. Then another. Within three days, I had over 60 downloads. Three people started a free trial. And then, I saw the first reviews pop up. Three of them, all five stars.

Reading those first reviews from complete strangers who loved the tool I had built was more rewarding than any metric. It was proof that the fight was worth it. This small success, born from the ashes of rejection, has been the single most powerful motivator for me to continue my iOS adventure.

This SHIP-A-THON, for me, isn't about the grand prize. It's about celebrating this journey. It's a testament to the idea that sometimes, the most important victory is simply refusing to give up. As they say, the treasure isn't the destination, but the process of getting there. And what a process it has been.

Technical Details & Architecture

While the story was an emotional rollercoaster, the app itself is built on a foundation of solid, modern engineering designed for performance, scalability, and a premium user experience. I approached this not just as a passion project, but as a professional product.

Core Architecture: Swift & SwiftUI

The app is built natively in Swift using SwiftUI for a modern, declarative UI, targeting iOS 17+. I followed a strict MVVM (Model-View-ViewModel) pattern to keep the business logic and data handling neatly separated from the UI, making the codebase clean, testable, and easy to iterate on.

  • Asynchronous Operations: I heavily leveraged Swift's modern concurrency with async/await for all network and database operations, ensuring the UI remains buttery smooth. @MainActor is used religiously to guarantee UI updates happen safely on the main thread.
  • Dependency Injection: Services like the API client and database manager are injected into the environment, promoting a modular and decoupled architecture.

The Magic: Multi-Language OCR & Live Camera Processing

The scanner is the heart of the app, and it's powered by a sophisticated stack:

  • VisionKit: I implemented an advanced VNRecognizeTextRequest configured to recognize text in over 7 languages (EN, JA, KO, ZH, FR, DE, ES). This includes a custom-built fallback system specifically for the complex character sets found in Japanese cards.
  • AVFoundation: This framework gives me direct control over the camera feed. To optimize performance, I implemented frame throttling, processing a frame only every 0.5 seconds to avoid draining the battery while still feeling instantaneous to the user.
  • Custom Extraction Logic: Raw OCR text is messy. I wrote a MultiLanguagePokemonExtractor service that normalizes names, handles special characters, and accurately extracts the card number (e.g., "123/456") from the recognized text blocks.

Backend Brains: Cloudflare & Dynamic APIs

To keep the app lightweight and secure, I avoided building a traditional, heavy server.

  • Cloudflare Workers: All my API keys and sensitive secrets are kept off the device. Instead, the app communicates with a lightweight Cloudflare Worker which acts as a secure proxy. This worker manages my keys and forwards requests to the appropriate third-party APIs. It's a fast, secure, and incredibly cost-effective serverless solution.
  • Multi-API Integration: The app dynamically fetches card and pricing data from multiple sources, including TCGdex, ensuring the most comprehensive and up-to-date information is always available.

A Polished Experience: UI, Caching, and Vibe

Getting the "vibe" right was crucial. I spent a lot of my focused coding time, often using tools like VibeCode to stay in the zone, just tweaking animations and flows until they felt perfect.

  • Design System: The UI is built on a custom, Pokémon-themed design system with a professional dark mode (#121212 backgrounds, #E3350D accents).
  • Smooth UI: I implemented shimmer effects and skeleton screens for loading states, so the user never sees a blank screen. All transitions are spring-based for a fluid, physical feel.
  • Comprehensive Caching: To minimize network requests and make the app feel instant, I built a multi-layered caching system for images, API search results, and pricing data.

Monetization & Analytics

The business logic is as robust as the technical foundation.

  • RevenueCat: I used RevenueCat to handle everything related to subscriptions. It made implementing a free trial, managing subscription status, and gating premium features (like detailed price history) incredibly straightforward.
  • Firebase Suite: Firebase Analytics, Crashlytics, and Remote Config are integrated to help me understand user behavior, instantly fix crashes, and tweak the app experience on the fly without needing to submit a new build.

Built With

  • cloudflare-workers
  • firebase
  • pokemonpricetrackerapi
  • revenuecat
  • swift
  • swiftui
  • tcgdexapi
  • vibecode
Share this project:

Updates