Shipping 14 tiny iOS utilities in a batch
I shipped 14 small iOS utilities to App Store Connect in a batch. Subnet calculators, a WoL remote, a JSON pretty viewer, a regex tester, a ping monitor, a few others. The interesting thing is mechanics — how do you batch-ship 14 binaries through the App Store without losing your mind — not any one of the apps individually.
The lineup
All bundle IDs under com.cwfrazier.*. Mostly priced $1.99 to $4.99.
One is $14.99 (the bigger one, a SSH-style terminal app, currently
in TestFlight under a separate company).
- LanScope — network scanner
- QR ToolKit Pro — QR generator/decoder
- SubnetCalc X — subnet calculator
- CleanDay — sober-day tracker
- WOL Remote — wake-on-LAN
- DNS Switch Pro — DNS profile switcher
- SpeedCheck Widget — speedtest as a home-screen widget
- PingBoard Pro — multi-host ping monitor
- JSON Pretty View — JSON viewer
- ConvertAll Units — unit converter
- RegExPad Pro — regex tester
- TurtleNotes — secure notes
- ColorCode Picker — color picker
- ShellPad — SSH terminal (separate company, TestFlight)
Most of these are the kind of utility I want as a sysadmin and nobody else writes the way I'd write it. None of them are revolutionary. All of them are "I'd pay $3 for the version I actually want."
The batch pipeline
The way to ship 14 apps without going insane:
One repo per app, identical layout. Each app is its own Xcode project, but the project structure is the same — same naming conventions, same script directory, same Info.plist patterns. A small change can be applied across all 14 with a script.
App Store Connect API for uploads. Bypass Xcode's
"Distribute App" wizard entirely. Generate an API key once, use it
from xcrun altool or xcrun notarytool plus
xcrun ascutil. Doing this for 14 apps from the Xcode UI would
take all afternoon. Via the API, it's a parallel batch script.
KEY_ID=...
ISSUER=...
KEY_FILE=~/.appstore/AuthKey.p8
for app in lanscope qrtoolkit subnetcalc cleanday wolremote dnschanger speedwidget ...; do
xcrun altool --upload-app \
--file build/$app.ipa \
--type ios \
--apiKey $KEY_ID \
--apiIssuer $ISSUER &
done
wait
Metadata as YAML in each repo. Description, keywords, support
URL, marketing URL — all in a single appstore.yml per app.
Submission tooling reads it.
Screenshots from a shared template. I have a Sketch file with device frames for the screenshot sizes Apple requires. Each app has a small images directory; the script composites them onto the frame and exports the right resolutions.
The annoying part
Screenshots are the bottleneck. App binaries can be uploaded en masse via API. Metadata can be set via API. Pricing can be set via API. Privacy questionnaire ("Data Not Collected" for all of these) can be set via API.
Screenshots have to be uploaded via API too, but they have to be at the right resolutions and they have to actually represent the app. The shared-template approach helps but the actual screenshot content — the screen state captured for each device size — is still manual per app.
Estimated time per app, after pipeline: ~15 minutes for screenshots, ~5 minutes for description tweaking, ~0 minutes for everything else. So a batch of 14 is about 5 hours of focused work, almost all of it on screenshots.
What I'd do differently
I'd start with a screenshot capture script per app, before the app
is "done." xcrun simctl io ... screenshot plus a list of
URL-scheme deep links into known UI states, run against the
simulator at each required device size, gives you the screenshot
set in a single batch with no manual driving. That's a one-day
infrastructure investment that I avoided because I figured "I'll
just take screenshots when I'm done." Wrong choice — screenshots
are the bottleneck and they're trivially scriptable, so the right
move is to script them once and stop touching them.
Also: pricing-decision exhaustion is a real thing. I priced $1.99 - $4.99 mostly by gut. For the next batch, I'd just pick one price and stick with it. The cognitive cost of "is this app a $2 app or a $3 app?" times 14 is more than the revenue difference will ever justify.