DEV Community

wayknow
wayknow

Posted on Originally published at wayknow.tech

I shipped my first iOS app solo — App Store Connect was harder than the app

I just shipped SnapCal, a barcode calorie tracker I built solo in about two weeks: SwiftUI, iOS 17+, zero third-party dependencies, 85 unit tests. Building it was fine. Getting it out the door taught me things no tutorial covers.

This is the list of what actually bit me — five traps, with the code and the commands.

1. The 404 that made my app look broken

SnapCal scans a barcode, asks Open Food Facts about it, then lets you log it. My HTTP layer was the standard shape:

private static func validateResponse(_ response: URLResponse) throws {
    guard let httpResponse = response as? HTTPURLResponse else {
        throw APIError.invalidResponse
    }
    guard (200...299).contains(httpResponse.statusCode) else {
        throw APIError.httpError(statusCode: httpResponse.statusCode)
    }
}
Enter fullscreen mode Exit fullscreen mode

And APIError.httpError rendered as "Server error (404)".

Here's the thing: Open Food Facts returns HTTP 404 with {"status":0,"status_verbose":"product not found"} for any barcode that is well-formed but not in the database. That's not a failure — it's the most common answer you'll get. Coverage is heavily skewed by country:

Region Products in OFF Have calorie data (sample of 100)
Global 4,759,550
France 1,268,282 92
USA 972,695 90
Japan 44,641
China 1,683 51

So the expected outcome for a random package is "no data", and my app was greeting users with a red "Server error (404)". A tester told me: "some barcodes work, some say something went wrong — your server logic is broken." He was right, but not the way he thought: the API was fine, my error taxonomy was wrong.

The fix is one early return, before validation:

let (data, response) = try await withRetry { try await session.data(for: request(for: url)) }

// OFF returns 404 for "well-formed barcode, not in the database" — that's
// "nothing found", not "something is wrong". Return nil so the UI can offer
// manual entry instead of a scary server error.
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 404 {
    return nil
}

try validateResponse(response)
Enter fullscreen mode Exit fullscreen mode

And in the view, "not found" gets its own state with a way forward:

if let food = await store.searchByBarcode(barcode) {
    scannedFood = food
} else if store.errorMessage == nil {   // nil meaning: no error, just no data
    showNotFoundAlert = true            // "Product not found" + [Add Manually]
}
Enter fullscreen mode Exit fullscreen mode

Then I went one step further: when you type in a food that isn't in the database, SnapCal stores it under that barcode in a dictionary of its own (userFoodsByBarcode, kept separate from the API cache so it never gets evicted). Scan the same product again and it resolves instantly, offline.

Takeaway: a "not found" is a state, not an error. If your empty state is dressed up as a failure, users will conclude your app is broken — and they'll be right about the experience even if they're wrong about the cause.

2. New developer accounts get asked for homework (Guideline 2.1)

The morning after I submitted 1.0, I got "Information Needed — Guideline 2.1". No bug, no rejection of the app itself. Apple wanted:

  1. a screen recording on a physical device, running the latest OS, starting from app launch
  2. purpose and target audience
  3. setup instructions / demo credentials
  4. every external service the app talks to
  5. regional differences
  6. anything regulated or third-party-licensed

If your app has accounts, UGC or paid content, the recording must show those flows — including account deletion, which is a hard requirement.

What I'd do differently: record that video before you submit. Mine was 3 minutes on an iPhone 15 Pro, and it was the only thing that took real time to produce after the fact. Everything else is copy: "no accounts, no login, no UGC, no IAP, so those flows don't exist; the only external service is Open Food Facts; nutrition data is ODbL v1.0."

Reply in the App Review conversation and paste the same text into App Review Information → Notes — Apple asks for both, and the notes carry over to future submissions. No new build is needed for a 2.1 reply; you just resubmit.

3. "Approved" is not "live", and my check was lying to me

I released 1.0, and then… nothing appeared. So I "verified" the store page by fetching it.

https://apps.apple.com/us/app/snapcal-barcode-calorie-log/id6812662105  → HTTP 200 ✅
Enter fullscreen mode Exit fullscreen mode

It was not the app page. My IP geolocates to a storefront where the app wasn't distributed yet, and instead of 404ing, Apple quietly serves that storefront's Today home page. The status code tells you the server answered — it doesn't tell you which page you got. Read the title and the content, or ask Apple's lookup endpoint about your bundle ID:

curl -s "https://itunes.apple.com/lookup?bundleId=tech.wayknow.snapcal&country=us"
Enter fullscreen mode Exit fullscreen mode

Even then, treat any single response as a hint rather than a fact — that index lags behind the storefronts and can be noisy. The ground truth for availability is App Store Connect → Pricing and Availability: mine listed 174 of 175 territories, with mainland China the only exception.

4. The app was missing from all of Europe, and nothing warned me

A couple of days after release, App Store Connect was still showing a banner I'd scrolled past: the EU Digital Services Act requires Apple to verify and display trader contact information for apps distributed in the EU, and an app with no declared trader status isn't distributed there at all. SnapCal simply wasn't reachable in any EU storefront.

The declaration lives at Business → Agreements → Compliance → Digital Services Act. Two choices:

  • "This is a trader account" — you enter address, phone and email, they get published on your App Store product page in the EU, you upload a document proving your business name and address, and you verify both email and phone with 2FA.
  • "This is not a trader account" — no contact information, click Done, you're finished.

SnapCal 1.0 is free, has no ads and no in-app purchases, so "not a trader" is the honest answer, and it's reversible. But the moment you sell anything — even a one-time unlock — you are a trader, and your address goes public across the EU. Worth deciding before you monetize, not after.

The storefronts came back within a day of declaring, and they came back on their own — no resubmission, no new build. SnapCal is now available in every storefront except mainland China, which needs an ICP filing.

5. Two submission-day traps

A pending agreement silently blocks every submission. Apple updated the Developer Program License Agreement. There's a banner about it on the Business page, and until the account holder reviews and accepts it, you cannot submit a new version. No error on the version page — the submit just isn't available. Fix: Business → Agreements → the "Free Apps" row → Review and Accept Terms.

Xcode Organizer can't see your archive. I built 1.0.1 with:

xcodebuild archive -project SnapCal.xcodeproj -scheme SnapCal \
  -destination 'generic/platform=iOS' -archivePath build/SnapCal.xcarchive
Enter fullscreen mode Exit fullscreen mode

Organizer only lists archives in ~/Library/Developer/Xcode/Archives/. So when I opened Distribute App → Upload, the only archive on offer was the old 1.0 build, and Apple rejected it:

This bundle is invalid. The value for key CFBundleShortVersionString [1.0.0]
must contain a higher version than that of the previously approved version [1.0.0]  (90062)
Invalid Pre-Release Train. The train version '1.0.0' is closed for new build submissions  (90186)
Enter fullscreen mode Exit fullscreen mode

Copy the archive into the folder Organizer watches, or just archive to the default location. And always confirm the version/build number Organizer shows before you hit Upload.

What I'd tell my past self

  1. Treat 404 as data, not failure — and give "not found" a real UI.
  2. Record the App Review video before submitting; new accounts get asked.
  3. Verify launch by reading the page, not the status code — and check availability in App Store Connect, not in a public API that lags.
  4. Declare the DSA trader status before launch; "not a trader" is reversible, "no declaration" costs you the EU.
  5. Accept pending agreements and double-check which archive you're uploading.

None of this is in the app. All of it decided whether anyone could install it.


SnapCal is free: barcode scanning, no ads, no account, and everything stays on your device. It's available in every storefront except mainland China. 1.0.1 (recently-logged foods in Quick Add, one-tap delete, better "not in the database" handling) is in review now. If you want to poke holes in it, I'm around — the App Store link is here.

This post also lives on the WayKnow blog.

Top comments (0)